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

# API Changelog

## V1 API Changelog

Track changes, additions, and deprecations to the V1 API.

***

### 2026-08-14 (`AddressV2` — a new address shape, rolling out field by field)

#### Breaking for address writers: `AddressV2` replaces the legacy `Address` primitive on the fields that have been migrated

`AddressV2` is a new built-in object primitive and the successor to `Address`.
It is **not** additive: the sub-fields are **renamed and re-nested**, so a
payload written for `Address` is not a valid `AddressV2`. Any integration that
writes an address field is affected the moment that field is migrated.

**The migration is per-field and per-company, not a global switch.** Fields move
from `Object: Address` to `Object: AddressV2` as each company is migrated, so a
single company can have some address fields on each shape at the same time.
There is no date on which "the API" changes.

**How to tell which shape a field takes — read the configuration, do not
assume.** Ask
`GET /api/v1/companies/{companyId}/entities/{entityType}/configuration` and look
at the field's `fieldType` (or `typeInfo.kind`):

* `Object: Address` → the flat legacy shape, unchanged.
* `Object: AddressV2` → the new shape below.

This is the only reliable signal. The endpoint, the entity type, and the field's
name all stay the same across the migration; only the configured type changes.

**What moved:**

| Legacy `Address` | `AddressV2`                                                                           |
| ---------------- | ------------------------------------------------------------------------------------- |
| `street`         | `enteredAddress.address1`                                                             |
| —                | `enteredAddress.address2` (new — unit / suite / floor)                                |
| `city`           | `enteredAddress.city`                                                                 |
| `state`          | `enteredAddress.state`                                                                |
| `zipCode`        | `enteredAddress.zipCode`                                                              |
| `country`        | `enteredAddress.countryCode` (renamed)                                                |
| `county`         | `geocode.county` — **provider output now, not something you enter**                   |
| —                | `entryMethod`, and the rest of `geocode` (status, provenance, coordinates, precision) |

**Before — a field typed `Object: Address`:**

```json theme={null}
{
  "mailingAddress": {
    "street": "350 5th Avenue",
    "city": "New York",
    "state": "NY",
    "county": "New York County",
    "country": "United States",
    "zipCode": "10001"
  }
}
```

**After — the same field typed `Object: AddressV2`:**

```json theme={null}
{
  "mailingAddress": {
    "enteredAddress": {
      "address1": "350 5th Avenue",
      "address2": "Suite 1200",
      "city": "New York",
      "state": "NY",
      "zipCode": "10001",
      "countryCode": "US"
    },
    "entryMethod": "manual"
  }
}
```

You do not send `county`, coordinates, or precision. Geocoding runs server-side
on save and fills `geocode` for you, and it never gates the write — a provider
miss or outage is recorded as an `unmatched` or `failed` geocode, not an error
on your request. If you already hold a geocode, send it under `geocode` with
`source: "provided"` and it is stored as given without a lookup; a `geocode`
sent with any other `source` is discarded and replaced by the platform's own
result.

**Reads.** Entity reads — fetching one entity, and the entity list endpoints —
return a field configured `AddressV2` in the `AddressV2` shape, including rows
that were written before the field was migrated, so you never have to handle
both shapes on one field. Reading `county` moves from the top level to
`geocode.county`, and `geocode.granularity` tells you how precisely the
coordinates (and therefore that county) were located.

**`zipCode` is still a string, and still the most common failure.** The path is
now `enteredAddress.zipCode`. JSON numbers cannot represent leading zeros, so
`02140` parses as `2140`; always quote it.

**Fields that have not been migrated are untouched.** `Address` stays fully
supported and is not deprecated — every field still typed `Object: Address`
behaves exactly as before.

Full reference: [AddressV2](/api-reference/object-primitives/overview#addressv2)
and the `Fmv1AddressV2` OpenAPI schema.

***

### 2026-08-14 (`fullTermPolicyInfo` is removed)

#### Breaking: the `fullTermPolicyInfo` container is gone from every policy and quote

`fullTermPolicyInfo` no longer appears on any policy response, in any persisted
policy segment we write, or in the framework configuration. The deprecation
notice in the 2026-08-13 entry below announced this; the container was a
platform-derived mirror, and once every value it carried had a root field there
was nothing left for it to say.

**Who this affects.** Any integration still reading a value out of
`fullTermPolicyInfo`. Writers are unaffected — the container has been read-only
and platform-derived since the term-bound cutover, so nobody sends it, and a
payload that still includes one is simply ignored.

**Where each member went:**

* `fullTermPolicyInfo.policyNumber` → root `policyNumber`
* `fullTermPolicyInfo.policyStartDate` → root `policyStartDate`
* `fullTermPolicyInfo.policyEndDate` → root `policyEndDate`
* `fullTermPolicyInfo.previousPolicyId` → **the relationships surface.** This is
  the one member with no root field, and the change the 2026-08-13 entry said
  would not happen before it had one. It has a better home instead: renewal
  lineage is the policy's own `previousPolicy` link, which you already send on
  `POST /transaction/renew` and which the platform writes as the
  `Policy<N:1:previousPolicy>Policy` relationship. Read the link back from the
  policy's `previousPolicy` field on any segment, or walk the chain from the
  expiring side. The container's copy was a display mirror of exactly that link
  and could never say anything the link does not.
* `fullTermPolicyInfo.primaryInsuredName` / `.primaryInsuredJoin` → root
  `primaryInsuredName` / `primaryInsuredId`. These left the container on
  2026-08-10; see that entry.

Each root field holds exactly the value its container member held, so every move
except `previousPolicyId` is a one-line change per read with no behavioural
difference.

**Historical data is untouched.** Policy segments and quotes written before this
change still carry the key in their stored JSON, and we neither rewrite nor hide
it there. What changed is that nothing on our side reads it and no response
carries it.

**Configuration.** `fullTermPolicyInfo` and the `FullTermPolicyInfo` custom
object are no longer framework-required rows, so a new company is provisioned
without them. An existing configuration that still declares them keeps importing
cleanly; the rows are ordinary tenant rows now, read by nothing.

***

### 2026-08-13 (the whole-term policy facts move to the response root)

#### Added: root `policyNumber`, `policyStartDate` and `policyEndDate` on every policy response

Every response that carried the policy number and the term bounds only inside
`fullTermPolicyInfo` now also carries them at the **root**:

* `GET /policies/list`, `GET /policies/versions`,
  `GET /policies/{policyId}/versions` — on each item's `summary`.
* `GET /policies/{policyId}/versions/{version}`.
* The five policy transactions — new business, renew, endorse, cancel,
  reinstate.

`policyNumber` is a string. `policyStartDate` and `policyEndDate` are the
structured [`Date`](/api-reference/object-primitives/overview#date) object
(`{day, month, year, timezone}`) — the same value the container holds, **not**
the ISO `startDate` / `endDate` fields beside them, which report the span the
version covers and shrink when a policy is cancelled. All three are `null` only
when the version carries no policy data.

Nothing was removed or renamed, so no integration breaks on this change. The
values are read from the same cross-segment-validated source the container is,
so a root field and its container copy can never disagree on one response.

#### Deprecated: `fullTermPolicyInfo` — read the root fields instead

`fullTermPolicyInfo` is deprecated on every policy response that carries it. It
is still returned, unchanged, during the transition window; it
will be removed once readers have moved off it (design of record: ADR 0043 —
term invariance is a per-field configuration property, and the container is
deleted).

**Who this affects.** Any integration that reads a value out of
`fullTermPolicyInfo`. Writers are unaffected: the container has been read-only
and platform-derived since the term-bound cutover, so nobody sends it.

**What you see now.** No change — the container is still present and identical.
The OpenAPI spec and the generated TypeScript client now mark it `deprecated`,
so a typed client will surface a deprecation hint at the call site.

**What to do — move each read to its root field:**

* `fullTermPolicyInfo.policyNumber` → root `policyNumber`
* `fullTermPolicyInfo.policyStartDate` → root `policyStartDate`
* `fullTermPolicyInfo.policyEndDate` → root `policyEndDate`
* `fullTermPolicyInfo.primaryInsuredName` / `.primaryInsuredJoin` → root
  `primaryInsuredName` / `primaryInsuredId` (already removed from the container;
  see the 2026-08-10 entry below)
* `fullTermPolicyInfo.previousPolicyId` — **not hoisted yet.** Keep reading it
  here; it is the one member without a root field, and the container will not be
  removed before it has one.

Each root field holds exactly the value its container member holds, so the move
is a one-line change per read with no behavioural difference.

***

### 2026-08-12 (a member `fullTermPolicyInfo` no longer declares is discarded, not rejected)

#### Fixed: an undeclared member of the derived container is no longer a `400`

`fullTermPolicyInfo` is platform-derived — every persisted write replaces it with
exactly its four framework members, so nothing you send inside it is ever read.
This reference has said as much since the cutover ("anything else you put in it
is discarded"), but the write path was **rejecting** an undeclared member instead
of discarding it:

```jsonc theme={null}
// what you got, on both policy transactions and entity CRUD
{ "error": { "code": "InvalidFieldModelV1Data",
             "message": "Unknown field 'fullTermPolicyInfo.primaryInsuredName'" } }
```

That fell hardest on callers of the entry below. The two retired primary-insured
members were deliberately left in already-stored policy segments and quotes, so a
client that read a policy, changed one field, and wrote the record back was handed
the retired keys by our read and then refused by our write.

An undeclared member of `fullTermPolicyInfo` is now dropped from the payload before
validation, on every write path. Nothing else moves: the four declared members are
still shape-checked, a term stated only inside the container is still rejected (the
term comes from the root `policyStartDate` / `policyEndDate`), and the other
containers — `fullTermPricingInfo`, `fullTermPolicyRatingResult`,
`crossSegmentRatingOutputs` — still reject an undeclared member, because those carry
facts you own rather than a mirror of ours.

***

### 2026-08-10 (the primary insured left `fullTermPolicyInfo`)

> Recorded on 2026-08-12. This change shipped on 2026-08-10 with no changelog entry;
> the notice below is the one that should have run with it.

#### Breaking: `fullTermPolicyInfo.primaryInsuredJoin` and `fullTermPolicyInfo.primaryInsuredName` are removed

Both members were dropped from every company's configuration. Sending either one
returned `400 InvalidFieldModelV1Data` — `Unknown field
'fullTermPolicyInfo.primaryInsuredJoin'` — from 2026-08-10 until the fix in the
entry above; from now on they are accepted and discarded, but they are still gone
from the contract and nothing reads them.

`fullTermPolicyInfo` is term-constant, so a primary insured recorded there was
frozen for the whole term — and a primary insured genuinely changes mid-term. The
container is now closed at four members (`policyNumber`, `policyStartDate`,
`policyEndDate`, `previousPolicyId`), and the primary insured is a per-segment fact
you can endorse like any other field.

**What to do:**

* **Remove both keys from your payload.** Nothing replaces them on the request.
* **The primary insured is derived, not sent** — it comes from the policy's own
  exposures, by the rule your configuration defines (typically the exposure your
  config marks as primary). Set the exposure; the platform resolves the insured.
* **Read it back** from the root `primaryInsuredName` / `primaryInsuredId` fields
  on the response — hoisted at the top level and present per segment. They resolve
  **per segment**, so a mid-term endorsement of the insured now takes effect from
  its endorsement date; the flat response fields show the last segment's insured.

Values already stored under the two removed keys were left in place rather than
migrated. Any policy you write again has its container replaced wholesale, which
clears the residue on that write.

***

### 2026-08-07 (policy invoices can be planned headlessly)

#### Added: explicit policy invoice plans on transactions and as a standalone batch

All five policy transaction endpoints now accept optional `invoicePlan`, an
explicit keep/void/create plan that commits atomically with the new policy
version. Omitting it preserves ordinary transaction behavior and creates no
invoices. When a pricing restatement touches a policy with active invoices, the
plan is required and must conserve the new pricing contract.

The same contract is available directly at
`POST /api/v1/companies/{companyId}/financials/policies/{policyId}/invoices/batch`.
Existing invoices omitted from `voidInvoices` are kept; every void must cite its
current `headJournalId`; dates, payees, and creates are never inferred. Its body
is exactly that plan, with no `author` field, and a company whose separate
`policy-financials` rollout gate is off receives `403` rather than `404`.

Quote binding also accepts `{ "generateInvoices": true }`. The server resolves
policy-invoice presets into that explicit plan before binding; omitting the flag
keeps bind behavior unchanged. Presets requiring a broker payee fail closed
until a canonical broker party is available.

Ad-hoc create or void requests against policy-linked invoice documents continue
to return `422 POLICY_INVOICE_BATCH_ONLY`, now with the standalone batch route as
the recovery path.

***

### 2026-08-06 (closing and re-opening an event is its own endpoint)

#### Added: `POST /events/{eventId}/close` and `POST /events/{eventId}/reopen`

An event (a claim or an incident) now closes and re-opens through dedicated
endpoints that take the date the action **takes effect**:

```
POST /api/v1/companies/{companyId}/events/{eventId}/close
POST /api/v1/companies/{companyId}/events/{eventId}/reopen
{ "effectiveOnDate": { "year": 2026, "month": 8, "day": 5 } }
```

Each moves `eventStatus`, stamps or clears the event's close date, and appends an
entry to its open/close-history log — in one transaction. Both require
`company.claim:create` and return `{ eventId, eventStatus }`.

#### Changed: `PATCH /entities/event/{entityId}` no longer changes `eventStatus`

A generic update that **changes** `eventStatus` is now rejected with **`409`**
(`GuardedStatusFieldWrite`), and the message names the endpoint to use instead.
The lifecycle dates a claim reports — opened on, previously closed on, re-opened
on — are derived from the open/close-history log, so a status change that skipped
the log would silently misreport them.

Two things are deliberately unaffected: setting `eventStatus` on **create** still
works, so a historical import can load claims that were already closed; and an
update that sends `eventStatus` with the value it already holds is still accepted,
so a client that echoes a whole record does not break.

If you close events through the API today, move those calls to the new endpoints.
The `x-computed-default` mark on `eventStatus` is unchanged and remains accurate
for creates — the new [Flow-written status
fields](/api-reference/entities/overview#flow-written-status-fields) section
explains the distinction.

#### Added: `409 EventLifecycleDateOutOfOrder`

Neither action may be dated before the last entry already in the event's log: a
close cannot land before the day the claim was opened, and a re-open cannot
precede the close it reverses. The same day is allowed. The error message names
the date the request has to clear.

***

### 2026-08-05 (the pricing contract replaces the billing container)

#### Changed: `fullTermPolicyBillingInfo` is renamed to `fullTermPricingInfo`, and its shape is new

The policy/quote billing container is now the **pricing contract**,
`fullTermPricingInfo`, in every request, response, saved view, and bordereau
field path:

* **Components with kinds.** The priced charges live in `pricingComponents`
  (the successor of `lineItems`). Each component is
  `{label, group, kind, value[, earningBasis]}`: `kind` is the new closed
  classifier (`Premium`, `Taxes`, `Fees`, `BrokerCommission`,
  `ProgramCommission`, `Other`); `group` is now only an invoice-grouping name;
  `value` is a **plain number** (the `{value, code}` Currency wrapper is gone —
  everything is USD); `earningBasis` (`pro-rata` or
  `fully-earned-at-inception`) is optional — omitted means pro-rata, and a
  basis is rejected on kind `Other`.
* **Server-computed, read-only rollups.** The container's five totals —
  `premium`, `taxes`, `fees`, `brokerCommission`, `programCommission` — are
  computed by the platform on every write, each the sum of its kind's
  components. They are never a contract input: a rollup value you send is
  ignored and recomputed.
* **`policyGrandTotal` is retired with no successor.** The container carries no
  grand total; sum the rollups you care about. (The RATING RESULT container,
  `fullTermPolicyRatingResult`, is unchanged and keeps its own
  `policyPremium`/`policyTaxes`/`policyFees`/`policyGrandTotal`.)
* The `Billing Table` display format is renamed **`Pricing Table`** in
  `/configuration` field locations.

#### Removed: the old key is no longer accepted in requests

The `fullTermPolicyBillingInfo` key — accepted (deprecated) during the
transition window on NEW\_BUSINESS/RENEW payloads and the
ENDORSE/CANCEL/REINSTATE channels — is now rejected like any other unknown
key/field. Stored data and every tenant configuration were migrated to the new
vocabulary, so reads never produce the old name either.

**What changes for you.** Nothing, if you already moved to
`fullTermPricingInfo` during the transition window.

* **Reading.** Read the container as `fullTermPricingInfo` everywhere the old
  key used to appear (version/transaction responses, the policy list summary,
  saved views, exports). Read totals from the five rollups; itemized charges
  from `pricingComponents`. Bordereau **column ids are unchanged**
  (`policyPremium`, `policyPremiumChange`) — only configured *field-column
  paths* move (e.g. `fullTermPricingInfo.taxes`).
* **Writing.** Send `pricingComponents` under `fullTermPricingInfo` (one
  component per charge, classified by `kind`) and let the platform compute the
  rollups. A payload naming `fullTermPolicyBillingInfo` now fails with a `400`
  (unknown key on the transaction channels; undeclared field inside
  `fieldModelV1Data.policy`).

***

### 2026-08-05 (policy field data is no longer nested)

#### Breaking: policy transactions send and return the field data unwrapped

The `fieldModelV1Data.policy` wrapper is gone from the policy-transaction
request and from every policy response. There is no accept-both window: send
and read the new shape.

**Requests** — `POST .../policies/transaction/new-business` and
`POST .../policies/transaction/renew` now take the policy field data as `data`,
a plain object, with the command metadata beside it:

```jsonc theme={null}
// before
{ "transactionTimestamp": "…", "fieldModelV1Data": { "policy": { "policyStatus": "active", … } } }
// now
{ "transactionTimestamp": "…", "data": { "policyStatus": "active", … } }
```

Both endpoints accept the same three keys — `transactionTimestamp`,
`displayAuthor`, `data`. (`displayAuthor` is new on `renew`; it already existed
on `new-business`.)

**Responses** — every policy read and write returns each segment as
`{ startDate, endDate, data }`. The old spelling was
`{ startDate, endDate, fieldModelV1Data: { policy: {…} } }` on the single,
versions, transactions and bordereau reads, while the **list** read already
returned the unwrapped object under the `fieldModelV1Data` name. Both are now
`data`, so the two read paths agree for the first time. This covers the list,
single, versions, transactions and bordereau reads and the MCP policy tools.

The three hoisted whole-term containers on a version response —
`fullTermPricingInfo`, `fullTermPolicyInfo`, `fullTermPolicyRatingResult` — are
unchanged, as are the hoisted `primaryInsuredName` / `primaryInsuredId`.

Entity CRUD is untouched: a Quote, Exposure or Event still carries
`fieldModelV1Data`. Only the policy surface changed, because only there did the
name wrap a second object.

**Error messages** no longer name the wrapper. A payload-shape failure on these
two endpoints now returns error code `InvalidPolicyData` (was
`InvalidFieldModelV1Data`) and names the field without a `policy.` prefix — for
example `'policyStartDate' is required (Date object)`. The generic
`InvalidFieldModelV1Data` code is unchanged for entity CRUD.

***

### 2026-08-05 (forms logic now guards form deletion)

#### Changed: `DELETE /forms/{number}` rejects dangling forms-logic references

Deleting a form now returns **`409`** with error code
`form-template-referenced-by-form-logic` when the live field-model configuration
still has a forms-logic rule for that form number. Remove the rule through the
configuration import flow, then retry the delete.

This guard also applies to deletion from the app's form library. It prevents a
form cleanup from leaving an invalid configuration that blocks a later config
import or config-sync reconciliation. Forms with no live forms-logic reference
continue to delete as before, and already-generated forms remain readable.

***

### 2026-08-05 (platform-generated fields are marked read-only in the schema)

#### Changed: a generated field publishes `readOnly` + `x-generated`

Some fields are **generated by the platform**: a sequence number minted when a
policy binds, an identifier composed from other fields. Originating or changing
one has always been rejected with a `400 GeneratedFieldWrite`. The
`/configuration` schema did not say so — a generated field could appear with no
`readOnly` mark, and one expressed as a keep-if-supplied calculation was published
as a **computed default**, whose documented meaning is "a value you supply is
kept". Both told you to send a field the API refuses.

Such fields now publish `readOnly: true` + a new `x-generated: true` mark, with a
`description` saying the value is generated and a supplied one is rejected. The
new [**Generated** write tier](/api-reference/entities/overview#field-write-tiers)
documents it, and the MCP `get_entity_schema` tool surfaces it as a `generated`
boolean.

**No rejection behavior changed** — a value the platform did not originate was
refused before and after, and resending the exact stored value is still tolerated
on an update. If a create or update of yours was failing with
`GeneratedFieldWrite` on a field the schema said was yours to set, this is why.
Drop the field from the payload and read the value back from the response.

One quiet fix rides along: an explicit `null` for a generated field **inside an
embedded exposure** used to blank the value on the embedded copy. It is now
ignored, so the platform's value survives.

Generated fields are a per-company configuration choice, so which fields carry
the mark depends on your configuration; read them from `/configuration` rather
than hard-coding a list. Fields inside an embedded **custom object** (a
sub-field) do not carry write-tier marks at all yet — that surface is unchanged.
An embedded **exposure** does carry them, in both of its modes.

***

### 2026-08-05 (bill review is callable over the API)

#### Added: `POST` and `GET /financials/invoices/{invoiceId}/bill-review`

A bill review checks one invoice against the company's own bill review rules and
reports what it thinks is wrong with it. Both halves are now on the API:

* **`POST …/bill-review`** starts one and answers **`202`** with the `runId` to
  poll. Nothing is reviewed inside the request — the review is background work.
  There is **no request body**, and no `actionId` or `If-Match`: the rules are the
  company's own (snapshotted when the run is created) and the invoice version is
  pinned from its current head, so there is nothing for a caller to send, and a
  review records a verdict beside the invoice rather than journaling an action
  against it. Requires `company.payment:update`.
* **`GET …/bill-review`** returns the **latest** run — `status`, the `passed`
  verdict, `error`, the pinned `invoiceHeadJournalId`, and the `findings`.
  Requires `company.payment:read`.

**Polling.** `status` is `queued`/`running` until terminal, then `succeeded` or
`failed`. `passed` is `null` until then — and stays `null` forever on a `failed`
run, so read `error` there rather than reading an empty `findings` list as a pass.

**Re-posting is safe.** While a review of the invoice is queued or running, a
second `POST` creates nothing and answers `202` with `outcome: in_flight` and the
id of the run already under way. Once that run is terminal the next call mints a
fresh one. A company at its background-work capacity gets a **`429`** and nothing
is queued.

**Staleness is yours to check.** `invoiceHeadJournalId` is the invoice version the
run judged; a review is never re-run because the document changed. Compare it with
the invoice's current `headJournalId` — if they differ, the findings describe an
earlier version.

Not offered: writing the rules, dismissing a finding, or reading earlier
runs. Rules and dismissals are managed in the app, and only the latest run per
invoice is exposed.

***

### 2026-08-03 (an embedded exposure no longer carries relationship fields)

#### Changed: relationship (join) fields are gone from the embedded-exposure object

An embedded exposure is a **point-in-time copy** of an exposure, taken once when it
is embedded and never re-copied. It now carries only the exposure's **own values**.
Its relationship fields — `contacts`, `exposureAssignee`, `referencingQuotes`,
`referencingPolicies`, `referencingSubmissions`, `referencingEvents`, and any other
field whose type is a join — are no longer part of that object, in either the
reference mode or the create mode, and no longer appear in the published schema.

They were never usable there. A copy cannot hold a relationship: the relationship
belongs to the exposure record itself, so the platform read these keys back as
`null` whatever you sent. Removing them makes the document match what actually
happens instead of advertising a field that silently did nothing.

**What changes for you.** Nothing, if you were not sending these keys.

* **Reading.** Read an exposure's relationships from the exposure itself
  (`GET /exposures/{id}`), not from a host's embedded copy. This is also the value
  you want: the exposure's current relationships rather than whoever was linked on
  the day the copy was taken.
* **Writing.** A relationship you send inside an embedded item is ignored rather
  than stored on the copy. To change a relationship, write the exposure
  (`PATCH /exposures/{id}`). When you create an exposure **inline** inside a host,
  a relationship you supply is still applied to the new exposure record — that path
  creates a real exposure, so the value lands on it and reads back from it.

Value fields are unaffected: you can still override any of them on an embedded
copy, and the override still never writes back to the exposure record.

***

### 2026-08-03 (data validation results are kept for the life of the run)

#### Removed: `410 Gone` on `GET /configuration/data-validation-runs/{runId}` and its `/findings` sibling

A data validation run's status *and* findings are retained for **the life of the
run**. The **`410 Gone`** response and its `DataValidationRunGone` error code are
**removed** from both by-id GETs, which now answer either `200` with the run or a
**`404`** for an id that belongs to no run of this company. A run that answered
`410` now answers `200`, so a branch on `410` — or on the `finishedAt` /
`retentionFloorDays` fields its body carried — is unreachable and can be deleted.

***

### 2026-07-31 (data validation results are retained for at least 30 days)

#### Added: `410 Gone` on `GET /configuration/data-validation-runs/{runId}` and its `/findings` sibling

A data validation run's results — the status counts *and* the findings — are now
documented as retrievable for **at least 30 days after the run completes**
(`finishedAt`). This is a **floor, not an expiry**: results may be kept longer, so
do not compute an expiry date from it.

Once a run has aged past that floor, both GETs answer **`410 Gone`** instead of
`404`. The distinction is worth handling: a `404` means the run id is wrong (check
it), while a `410` means the id is right and the answer is simply no longer
available (start a new run) — so a client holding a good run id is not sent off to
debug the id. The `410` body carries the run's `finishedAt` and the
`retentionFloorDays` it fell outside of, neither of which a `404` ever carries.

Three properties to build against. The two endpoints **age out together**, so a run
is never readable through one and gone through the other. A run id belonging to
another company is still a `404`, never a `410`, so this response can never confirm
that an id exists elsewhere. And a run that has **not finished** is never aged out,
however long ago it was started.

***

### 2026-07-30 (data validation findings — which records would break, and why)

#### Added: `GET /configuration/data-validation-runs/{runId}/findings`

The per-record detail behind a data validation run (permission
`company.configuration:export`), completing the three-endpoint set below. It
returns the standard `{ items, totalCount }` envelope, one item per stored record
the run judged **not** to adhere: `entityType`, `entityId`, the undeclared
`extraKeys` it holds, and the `violations` whose values would fail a write. Read a
violation's `fieldPath` — ordered, machine-stable segments — rather than splitting
the legacy dotted `field`; both are `[]` / `null` for a record-level failure that
names no single field.

**It is paginated, and separate from the poll, on purpose.** A status poll is made
roughly once a second, and a run over a broken book can produce tens of thousands
of findings, so the poll stays fixed-size and the findings are pulled here with
`page` / `pageSize` (default 50, maximum 500 — a larger `pageSize` is clamped, not
rejected).

`totalCount` ignores the pagination, but it is only **stable once the run has
reached a terminal status** (`succeeded` or `failed`). A run still `queued` or
`running` appends findings as it scans, so page 1 of a live run can report a
smaller `totalCount` than page 2. Findings only ever append, never reorder, so the
ordering of what you have already read stays put. To size a whole pull from the
first page, poll `GET /configuration/data-validation-runs/{runId}` until the status
is terminal, then read the findings.

**`entityType` is how you group.** The response is a flat list — request one entity
type at a time to build a per-type view. The filter narrows `totalCount` as well, so
the count always describes the items beside it. An `entityType` the run found no
problem in is a legitimate empty page; an `entityType` that is not a real entity
type is a **`400`**, not an empty page, because `?entityType=Policies` answering
`{"items":[],"totalCount":0}` would read as "none of my Policy records have a
problem". One field is worth calling out: **`adheres` is always `false`** on a
returned finding — a row exists only for a record that did not adhere.

***

### 2026-07-30 (data validation runs — scan stored records against a configuration)

#### Added: `POST /configuration/data-validation-runs` and `GET /configuration/data-validation-runs/{runId}`

Two endpoints (permission `company.configuration:export`) that check whether the
records a company has **already stored** would still fit a configuration — the
data-aware companion to **validate**, which reads no records at all.

The start endpoint's request body is **optional, and that is how you choose what
to scan**: send a complete configuration body to scan that candidate without
importing it, or **send no body at all** to scan the company's live configuration.
It returns `{ runId, outcome }` immediately; the scan runs in the background and
`GET .../{runId}` reports `status` (`queued` → `running` → `succeeded` / `failed`)
plus running counts of records scanned, adhering, holding undeclared keys, and
breaking. There is deliberately no total or percentage, and per-record detail is
not part of the response.

Two behaviours to build against. Starting a run whose configuration matches one
already in flight returns that run's id with `outcome: "duplicate"` rather than
starting a second scan — a successful `200`, not an error, so a retrying client
should poll the id it gets back; pass `?force=true` to start a new run anyway. And
a company may have **3** runs in flight at once: a 4th distinct configuration is
refused with a `429` whose body carries `inFlightRunIds`. `?force=true` does not
lift that limit.

***

### 2026-07-30 (correction: rating and tax output containers are settable again)

#### Fixed: `exposureRatingResponse`, `crossSegmentRatingOutputs` and `inscipherTaxPlan` are no longer system-owned

The 2026-07-24 embedded-exposures write-tier entry below listed these three
Exposure fields as **system-owned**, and a later change began
enforcing that on write: the value was silently dropped from an embedded-exposure
item instead of stored. **That was wrong, and it is reverted.** All three are back
to **settable**, unmarked in the published schema, and stored as sent.

The tier was a mistake because the platform does not persist these values itself.
Hosted rating is stateless — it computes and returns results without saving them —
so the payload that writes them back is their only source. Marking them read-only
removed the sole copy and left the exposure's rating fields null.

**No action is needed if you never stopped sending them.** If you removed them
from your payloads after that entry, resume sending them and re-save any
quote whose exposure-level rating values are now empty. The documented
[external-rating flow](/api-reference/rating/overview) — `PATCH` the quote with
`policyRatingResponse` and `exposureRatingResponse` populated — is correct and
supported.

Whether a future release moves rating persistence onto the platform is an open
design question; it will get its own entry, with notice, if the contract changes.

***

### 2026-07-28 (read-only smart-tag audit for forms and templates)

#### Added: `GET /forms/template/{number}/smart-tag-audit` and `GET /forms/generated/{id}/smart-tag-audit`

Two read-only endpoints (permission `forms:read`) that classify every `AII…`
smart-tag identity a stored document carries — live anchors and dormant
`DOCVARIABLE` authoring codes alike — against the company's current field
configuration: `resolvable`, `framework`, `legacy-renameable`,
`legacy-orphan`, `dead-hashed`, or `foreign`. Nothing is written. A healthy
document reports only `resolvable`/`framework`; anything else names a tag
that will render unfilled on generated documents and how to interpret it.

***

### 2026-07-27 (record payment — explicit allocations or the whole balance)

#### Breaking: `POST /financials/invoices/{invoiceId}/payments` replaces `amountCents` with two modes

The record-payment request no longer takes a document-level `amountCents`. The
field is **removed**: sending it is now a `400`. Every request must instead carry
exactly one of two modes — both together, or neither, is a `400`:

* **`payBalanceDue: true`** — settle the whole document. The server marks every
  open line item at its full remaining, in the document's line-item order, in
  **both directions at once**. The invoice comes back `paid` with
  `balanceDueCents: 0`, and the gesture's net cash equals the `balanceDueCents`
  you asked to pay. Only the literal `true` is accepted; `payBalanceDue: false`
  is a `400`.
* **`allocations`** — an array of `{ lineItemId, amountCents }`, naming exactly
  the line items to mark and exactly the cents for each. A line you do not name
  is untouched; a line you name for less than its remaining stays open.

`allocations[].amountCents` is in the **line's** frame — it settles that line's
remaining toward zero, so it carries that remaining's sign. This is not the
oriented net-cash frame `balanceDueCents` reads in: a receivable-direction line
with 3,000 remaining takes a mark of `+3000`, even though the cash moves in.
Each amount must be a nonzero integer, the array must be non-empty, and a
`lineItemId` may appear at most once per request (each a `400`).

The `422` guards are unchanged and now apply to the marks you author:
`UNKNOWN_ID` for a line item not on the document, `INVALID_AMOUNT` for a mark
opposing its line's remaining (or a `payBalanceDue` against a document with
nothing open), and `AMOUNT_EXCEEDS_BALANCE_DUE` for a mark overshooting its
line's remaining. The bounds are per line, not per document — an allocation well
inside the document's balance due is still rejected if it overshoots the line it
names.

Everything else on the endpoint is untouched: `If-Match`, the `actionId`
idempotency key, the auto-approval behaviour, `erodeReserves`, and the response
shape (`paymentIds` + `createdPayments`, now in mark order — the `allocations`
order, or line-item order under `payBalanceDue`). Reusing an `actionId` with
different `allocations` is `409 ACTION_ID_REUSED`.

**To migrate.** A call that settled a whole invoice becomes
`payBalanceDue: true`. A call that paid part of one becomes an `allocations`
array naming the lines — read the invoice's `lineItems` and its live `payments`
to see what each line has remaining. There is no transition period: the old
field stops being accepted with this change.

***

### 2026-07-27 (legacy financials endpoints removed)

#### Removed: the deprecated legacy financials endpoints

Every company now runs on the current financials surface, so the deprecated
legacy endpoints are gone from the API:

* **`GET /financials/invoice-drafts/{invoiceId}`** and
  **`POST /financials/invoice-drafts/{invoiceId}/finalize`** → a draft is an
  ordinary invoice with `stage: "draft"`; use
  `GET /financials/invoices/{invoiceId}` and
  `POST /financials/invoices/{invoiceId}/finalize`.
* **`POST /financials/event-transactions/import`** → use
  `POST /financials/events/{eventId}/import`.
* **`GET /financials/invoice-types`** → use
  `GET /financials/config/categories`.
* **`POST /financials/payees/reassign`** → use
  `POST /financials/payees/{payeeId}/merge`.

Each already answered `404 Not Found` for every company, so no working
integration changes.

***

### 2026-07-27 (file categories — configured per entity type)

#### Added: `entityType` on List File Categories

`GET /files/categories` accepts an optional `entityType` query parameter (the
lowercase kebab owner slug: `company`, `event`, `exposure`, `quote`, `policy`,
`submission`, `person`, `organization`). With it, the response adds the requested
`entityType` and an `entityTypeCategories` array of `{ name, configured }` — that
entity type's admin-configured categories in their configured order, followed by
the labels in use on its live placements that no configured category covers, and
`categories` carries the same names in the same order. Admins manage these lists
in Company Settings → File Categories; each entity type has its own.

Without the parameter the response is unchanged: `categories` alone, the distinct
values in use across the whole company. Category writes are still free text —
**Update File** and **Update File Placement** accept any label up to 255
characters and are not validated against the configured lists.

***

### 2026-07-27 (required input — one complete 400)

#### Changed: a create missing required input is rejected once, naming every field

An entity create that omits required input is now rejected **before anything is
validated or written**, with a single `InvalidEntityShape` 400 whose
`userMessages` name **every** missing field — instead of the previous
one-field-at-a-time message raised late in the write. The problem code is
unchanged, so existing error handling keeps working; only the timing and the
completeness change. A payload that is both incomplete and wrong-typed now
reports the missing input first — supply it, resubmit, and the remaining checks
run as before.

#### Changed: `required` in the configuration schema is now the create contract

The `required` array of `GET /entities/{entityType}/configuration` is now exactly
the set of fields **you** must supply on a create: the platform's structural
requirements minus everything the write path produces for you (join fields,
calculated fields, server-seeded defaults). Previously it echoed the fields your
configuration marks required **in the UI**, which could both demand fields the API
fills in for you (e.g. `quoteNumber`, `eventStatus`) and omit fields a create
genuinely needs. The two now agree by construction: comply with the published
`required` and a create cannot be rejected for missing input. `policy` is
unaffected — it has no generic create endpoint, so its `required` still reflects
UI requiredness.

***

### 2026-07-24 (bulk wipes — delete-guarded)

#### Changed: both bulk wipes now consult the company delete guard

`POST /financials/deleteAll` and `POST /entities/{entityType}/deleteAll` are
now gated by the **company delete guard** — the same mechanic that gates the
internal admin wipes. A company whose guard is `active` or
`onboarding-active` (the default posture for a live company) is rejected with
`409 DeleteGuardConflict` and nothing is deleted, regardless of the key's
permissions. To run either wipe, a non-active delete guard
(`onboarding-allows-delete`, `demo`, `internal-dev`, or `inactive`) must
first be set from the Control Plane. The financial guard on the entity wipe
(`409 DeleteBlockedByFinancials`) is unchanged.

***

### 2026-07-24 (bulk wipes — clear-financials + financial guard)

#### Added: Delete All Financial Data (`POST /financials/deleteAll`)

New SUPER\_ADMIN-only endpoint (`company.financial-data:deleteAll`) that
hard-deletes every financial **record** the company holds — invoices,
payments, journal, ledger, and per-entity balances — while keeping financial
**configuration** (transaction categories, line item types, approval config).
Returns `{ deletedRecords }`. This is the first rung of the breaking-config
reset ladder: **clear financials → per-type entity `deleteAll` → re-import**.

#### Changed: `POST /entities/{entityType}/deleteAll` is financially guarded

The per-type bulk entity wipe now rejects with `409 DeleteBlockedByFinancials`
if financials holds a live claim on **any** record of the type — a live linked
invoice (voided invoices included) or a nonzero entity account balance. The
rejection is all-or-nothing (nothing is deleted) and carries a structured
`financialBlockers` object: `liveInvoiceCount`, `nonzeroBalanceCount`,
`blockedEntityCount`, and up to 10 `sampleBlockedEntityIds`. Clear the
company's financial records first (`POST /financials/deleteAll`), then retry.
The breaking-config import `409` guidance now names this order.

***

### 2026-07-24 (embedded exposures)

#### Documented: embedded-exposure fields are a two-mode contract

The `/entities/{entityType}/configuration` schema now renders an embedded-exposure
field (an exposure embedded in a Quote, Policy, or Submission) as a `oneOf` of two
modes: **reference** an existing exposure by `id` (with optional per-field
overrides) or **create** a new exposure inline (no `id`). Providing the field
restates the complete membership; each item is validated as-if-inserted against
the current Exposure configuration, with structured `400`s naming the offending
item. See [Entities → Embedded exposures](/api-reference/entities/overview#embedded-exposures).

#### Documented: fields advertise a write tier (`x-system-owned` joins `x-calculated`)

Every field in a `/entities/{entityType}/configuration` schema now advertises its
**write tier** so a caller can tell a settable input from one the platform owns.
The full field list is unchanged — the schema stays complete for reading — but a
**system-owned** field is now marked `readOnly: true` + `x-system-owned: true`
(alongside the existing **calculated** mark `readOnly: true` + `x-calculated:
true`; a field may carry both). System-owned covers the values the platform, not
the caller, is the source of truth for: the Exposure reverse-listing joins
(`referencingEvents` / `referencingQuotes` / `referencingPolicies` /
`referencingSubmissions`), the rating-output containers
(`crossSegmentRatingOutputs`, `exposureRatingResponse`), and `inscipherTaxPlan`.
The marks appear on the top-level schema and inside both embedded-exposure modes.

A fourth tier corrects a false read-only signal: a **computed-default
(caller-wins)** field — one calculated by the self-referential keep-if-supplied
idiom `IS_PRESENT(<field>) ? <field> : <default>` (e.g. `quoteNumber`,
`quoteStatus`, `eventStatus`) — is now marked `x-calculated: true` +
`x-computed-default: true` and is **no longer `readOnly`**. The server fills a
default only when the field is omitted and keeps a value the caller supplies, so
publishing it read-only was wrong. A plain calculated field (any other fallback,
or a condition gated on a different field's presence) keeps `readOnly` +
`x-calculated`. System ownership wins over computed-default (a system-owned field
stays `readOnly`). The MCP `get_entity_schema` slim surfaces the tiers as
`systemOwned` and `computedDefault` booleans. Documentation/annotation only — no
wire shape or validation behavior changed. See [Entities → Field write tiers](/api-reference/entities/overview#field-write-tiers).

<Note>
  Since 2026-08-06 this is only half true of `eventStatus`, which is settable on
  **create** but can no longer be *changed* by an update — see [Flow-written
  status fields](/api-reference/entities/overview#flow-written-status-fields).
</Note>

***

### 2026-07-24 (financials — pre-rollout)

> Financials V2 is pre-rollout: the published contract updates in place ahead
> of the enablement flip. No live external consumer exists yet, so semantics
> changes are legal in this window; it closes at the flip.

#### Changed: posted line items are immutable — `LINE_ITEMS_IMMUTABLE` replaces `ITEM_BELOW_ALLOCATED`

Once an invoice is **posted** (a non-draft creation, or finalize for drafts),
its line items — ids, types, amounts — and its category are **fixed**.
**Update Invoice** (`PUT /financials/invoices/{invoiceId}`) still carries the
full document, but the line-item set and `categoryId` must echo the stored
document verbatim; the mutable remainder is `incurredDate`, `dueDate`, `memo`,
`fieldData`, and per-line memos. Any post-posting line-item or category change
is rejected `422 LINE_ITEMS_IMMUTABLE`, **at any payment count — zero
included**. Corrections are void-and-recreate, payments removed first. Drafts
are untouched: a draft's document (line items and category included) replaces
wholesale until finalize.

The stable 422 catalog stays at **18 codes**: `ITEM_BELOW_ALLOCATED` (per-line
signed cover — unreachable once posted documents cannot change) leaves the
enum, `LINE_ITEMS_IMMUTABLE` joins it. A category change no longer answers
`LIVE_PAYMENTS` (that code remains for re-link, payee change, and void under
live payments).

#### Added: `approved` and `draftAmountPaidCents` on the invoice row

The invoice representation documents two fields the wire already serves:
`approved` (boolean — the approval projection: set by approve, cleared only
by unapprove; an edit never resets it) and `draftAmountPaidCents` (integer,
nullable — a DRAFT's annex-derived paid total in the same oriented net-cash
frame as `amountPaidCents`; `null` on non-draft invoices). Documentation
only — no wire change.

***

### 2026-07-23 (files)

#### Added: Company Files version history + re-upload

**`GET /api/v1/companies/{companyId}/files/{fileId}/versions`** lists a file's
version history — every finalized (`ready`) version plus any in-flight
`pending` re-upload, newest first, with the current version flagged. Each row
(`versionId`, `fileName`, `contentType`, `byteSize`, `state`, `isCurrent`,
`createdAt`) describes that version's immutable bytes. Requires
`company.file:read`.

**`POST /api/v1/companies/{companyId}/files/{fileId}/versions`** mints a
re-upload intent — a new version of an existing file. It returns
`{ fileId, versionId, uploadUrl }` exactly like a first upload intent; `PUT`
the bytes to `uploadUrl`, then finalize with the **existing**
`POST /files/{fileId}/finalize` (there is no new finalize surface). Finalize
repoints the file at the new version, which silently becomes current — the
file keeps its id, placements, categories, and history. Because it changes an
existing file, it is gated by `company.file:update`, not `company.file:create`.

This closes the gap where a third-party consumer had to delete and re-create a
document to update it, losing its placements, categories, and history.

#### Added: Restore a previous file version

**`POST /api/v1/companies/{companyId}/files/{fileId}/versions/{versionId}/restore`**
rolls a file back to an earlier version by repointing it at the named version —
pure metadata (no bytes move, nothing is deleted, the previously-current
version stays in history). It returns `{ fileId, currentVersionId }`. Only a
`ready` version can be restored; restoring a `pending` version or the version
that is already current returns `409`, and an unknown version returns `404`.
This is the rollback counterpart to re-upload — a consumer that pushed a wrong
new version now has a way back. Gated by `company.file:update`.

#### Added: File category vocabulary

**`GET /api/v1/companies/{companyId}/files/categories`** returns
`{ categories }` — the distinct category values in use across the company's
live file placements, sorted case-insensitively. Because `category` is
free-text, a consumer writing categories via **Update File** / **Update File
Placement** can now draw from the existing, company-wide vocabulary instead of
fragmenting it. Requires `company.file:read`.

#### Added: Batch download URLs

**`POST /api/v1/companies/{companyId}/files/download-urls`** mints signed read
URLs for up to 100 files in one round trip — the batch counterpart of
`GET /files/{fileId}/download-url`, for fetching an entity's whole document set
without one request per file. It returns `{ downloadUrls }`, each item pairing a
`fileId` with the same per-file fields as the single-file endpoint (`url`,
`expiresAt`, `fileName`, `contentType`, `byteSize`); an optional `disposition`
(`attachment` default, or `inline`) applies to the whole batch. It is
**all-or-nothing**: if any id is unknown, cross-company, or not `ready`, the
whole request is rejected (`404`/`409`) and no URLs are returned. An empty
`fileIds` array or more than 100 ids returns `400`. Requires
`company.file:download`.

#### Added: Bulk move files

**`POST /api/v1/companies/{companyId}/files/bulk-move`** moves a batch of an
owner's files into one folder (or to the owner's top level with
`folderId: null`) in a single transactional request — reorganizing an entity's
whole document set without one `PATCH` per file. The request names the owner
(`entityType` + `entityId`) whose placements move; only that owner's placement
of each file moves, so a file shared onto other entities keeps its placements
there. The target folder must belong to the same owner. It returns
`{ ids }` — the ids of the files whose placement moved. It is
**all-or-nothing**: if any id is unknown, cross-company, or not placed under
the owner, the whole request is rejected (`404`) and nothing moves; a target
folder owned by someone else is a `400`. An empty `fileIds` array or more than
100 ids returns `400`. Requires `company.file:update`.

***

### 2026-07-23 (later still)

#### Changed: payments are per-line settlement MARKS — record-payment response reshape

**`POST /api/v1/companies/{companyId}/financials/invoices/{invoiceId}/payments`**
— the request is unchanged in shape (still one scalar `amountCents`, no
per-line input — an explicit per-line array remains a schema-level `400`),
but its meaning and its response are reshaped. `amountCents` is now
**oriented net cash** (positive = out, negative = in — the frame
`balanceDueCents` reads in), and the server fans it into **per-line
settlement marks** pro-rata over the open lines of the scalar's OWN
orientation — a net-outflow payment prorates against payable-direction
(outflow) lines, a net-inflow payment against receivable-direction (inflow)
lines; the non-matching direction never receives partial payments via this
API. Worked: 8,000 expense + 3,000 income, pay 2,000 → the 2,000 prorates
against the outflow lines only — expense remaining 6,000, income untouched,
net balance 3,000. The response's top-level `paymentId` is **replaced by
`paymentIds` + `createdPayments`** — the created mark rows, each id a
removal handle. The `actionId` anchors the first mark's journal action;
sibling marks mint their own ids in the same transaction, and an identical
retry replays the full set.

The payment row — in the detail read, invoice write responses, and the
record-payment response — is now a MARK: it **gains `lineItemId` and drops
`allocations`** (`amountCents` is signed in the line's frame and settles
that line toward zero). The create endpoint's `draftPayments` annex entries
likewise now **require `lineItemId`**. The payee merge and `movePayments`
re-records copy amount / date / memo / **line item** / erode flag verbatim.

`INVALID_AMOUNT` and `AMOUNT_EXCEEDS_BALANCE_DUE` keep their codes with
**per-line meaning**: a zero mark or one whose sign opposes its line's open
remaining (a scalar with no open lines in its direction included), and a
mark whose magnitude overshoots its line's remaining. There is no
document-scalar payment bound anymore.

#### Changed: document scalars are ORIENTED; settlement is per line

On every invoice representation, `totalAmountCents`, `amountPaidCents`, and
`balanceDueCents` are now **net-cash figures**: line items and marks count
oriented by their types' directions (payable +, receivable −), so
`balanceDueCents` is the net cash remaining to move — positive = out,
negative = in — and may move **non-monotonically** as opposite-direction
lines settle. Status derives **per line** (first match): `no_charges` iff
every line's amount is zero → `paid` iff every line is settled → `owed` iff
there are no live marks → else `partially_paid` — a zero-due document with
unsettled lines reads `owed`, and no scalar can make a document `paid`.
`paidDate` is the payment date of the mark that first made every line
settled. Field names, the status enum, and the invoice row's shape are
unchanged — the values' meaning changed.

#### Changed: remaining reserves are SIGNED — three `422` codes retired (enum lands at 18)

The stable precondition enum drops **`TOTAL_BELOW_AMOUNT_PAID`** (cover is
per line — `ITEM_BELOW_ALLOCATED`, signed cover over a line's live marks,
is the one update-time cover guard), **`EXPECTED_TOTAL_BELOW_PAID`**, and
**`ERODES_BELOW_ZERO`** — and now documents the full stable vocabulary of
18 codes: the draft-lifecycle guards **`INVOICE_DRAFT`** /
**`INVOICE_NOT_DRAFT`** and the approvals guards **`UNAPPROVE_PAID`** /
**`NOT_APPROVED`** (money cannot post against an unapproved invoice while
the approvals feature is on; an API caller cannot self-approve) join the
published enum they were missing from. A reserve scope's only law is the
identity
Reserves + Paid = Expected Total, at every sign: **`PUT
…/events/{eventId}/reserves/{categoryId}`** now accepts an expected total
below the scope's paid-to-date (`200`; the remaining reserve reads
negative — over-paid against a standing estimate), the import composition's
`reserves.set` members may land the same state, eroding payments apply to
the remaining reserve **unbounded** (eroding past the estimate takes it
below zero, never a `422`), and a re-link `movePayments` with
`erosion: "preserve"` **succeeds without destination headroom** — the
destination scope's remaining reserve goes negative (send
`erosion: "none"` or set the destination's estimate first if that reading
is not intended; previously a shortfall rejected the whole request).

Financials V2 is pre-rollout: the published contract updates in place ahead
of the enablement flip, so no live consumer sees a shape or behavior change —
semantics changes are legal in this window.

***

### 2026-07-23 (later)

#### Changed: balance account metadata — `normalBalance` replaced by `direction` + `lineItemTypeId`

**`GET /api/v1/companies/{companyId}/financials/balances`** and
**`GET …/financials/entities/{entityType}/{entityId}/balances`** — on every
balance account detail row (the `accounts[]` entries and `cash`),
`normalBalance` is **replaced by `direction` + `lineItemTypeId`**.
`direction` is the account's provenance direction (`payable` |
`receivable`): a `line_item` leaf's or payable/receivable twin's line item
type direction, a reserves/unpaid/additional account's category
`expectedDirection` — and `null` exactly for the cash account.
`lineItemTypeId` is set on `line_item` leaves and their payable/receivable
balance twins (the per-line-item join handle), `null` elsewhere. Orientation
now derives from **role × direction**: `cash`, `receivable` twins, and
receivable-direction reserves/unpaid sit on the assets side;
payable-direction leaves and `additional` on the expenses side (both carry
debit); payable twins and payable-direction reserves/unpaid on the
liabilities side; receivable-direction leaves and `additional` on the income
side (both carry credit). Amounts are unchanged — still raw signed cents in
the ledger convention; only the metadata changed.

Financials V2 is pre-rollout: the published contract updates in place ahead
of the enablement flip, so no live consumer sees a shape change.

***

### 2026-07-23

#### Changed: Signed payment amounts — credit documents settle toward zero

**`POST /api/v1/companies/{companyId}/financials/invoices/{invoiceId}/payments`**
— `amountCents` is now **signed and nonzero, settling the invoice toward
zero**: it carries the open balance's sign and its magnitude may not exceed
the balance's (was: positive up to the balance due; on an ordinary
all-positive invoice the rule reduces to exactly that). Two `422` codes are
re-scoped with **no new codes and no enum change**: **`INVALID_AMOUNT`** now
means a zero payment, a payment whose sign opposes the open balance — any
nonzero payment on a zero balance included (previously
`AMOUNT_EXCEEDS_BALANCE_DUE`) — or an allocation sign-inconsistent with its
line item; **`AMOUNT_EXCEEDS_BALANCE_DUE`** is narrowed to pure magnitude
overshoot.

Credit/reversal invoices are now first-class end-to-end: an invoice may
carry a negative total, a negative line `amountCents` posts opposite the
line item type's expected direction (the posting rule still comes from the
type, never the sign), and negative payments settle such documents toward
zero. The update-time cover guards (`TOTAL_BELOW_AMOUNT_PAID` /
`ITEM_BELOW_ALLOCATED`) compare sign and magnitude accordingly. The
reserve-eroding guard is now **oriented**: only a payment whose net effect
reduces the remaining reserve is bounded by headroom
(`422 ERODES_BELOW_ZERO`) — a reversal receipt is never blocked.

No wire shapes or enum values changed — request/response schemas are
byte-identical and this is a description-level contract change only.
Financials V2 is pre-rollout: these semantics land in the published contract
ahead of the enablement flip, so no live consumer sees a behavior change.

***

### 2026-07-22 (later)

#### Added: `movePayments` on invoice re-link — move a paid invoice in one call

**`POST /api/v1/companies/{companyId}/financials/invoices/{invoiceId}/relink`**
accepts an optional `movePayments` object. A re-link normally requires zero
live payments (`422 LIVE_PAYMENTS`); opting in composes the paid-invoice flow
atomically — every live payment is removed, the invoice re-links, and each
payment is re-recorded (amount, date, memo, allocations copied verbatim under
fresh `paymentId`s) against the new link. `movePayments.erosion` picks how
re-records treat reserves: `preserve` (default — original erode flags kept,
destination headroom required, `422 ERODES_BELOW_ZERO` on a shortfall) or
`none` (all re-records post non-eroding). `journalIds` returns every emitted
action in execution order.

***

### 2026-07-22

#### Added: optional `author` display label on the existing financials writes

Every financials write available in this release accepts an optional
**`author`** field (1–255 characters) — a display label for the source system's
author (see the
[Financials overview](/api-reference/financials/overview)'s *Authorship*
design rule). Where the app shows who recorded a change, a labeled row reads
as your label and an unlabeled row reads as the built-in "External API"
actor; rows written through the API are always visibly marked as API-written
regardless of the label. The reserve-update feed rows now return the label as
**`displayAuthor`**, and their `createdBy` is always a real user id (the
"External API" actor for API writes) — it is no longer ever `null`.

The policy-invoice batch added on 2026-08-07 is the later exception: its
detached plan has no `author` field.

#### Added: Financials write surface — the full financials API contract

The financials API is now the complete read/write contract of the financials
subsystem (see the reworked
[Financials overview](/api-reference/financials/overview) for the design
rules: client-supplied `actionId` idempotency, the `If-Match` concurrency
watermark, stable `422` precondition codes, cursor pagination, integer-cent
amounts). Companies are enabled progressively — until a company's cutover,
these endpoints return `404` for it and the legacy financials endpoints keep
serving it.

Nine single-invoice writes (`company.payment:update`):

* **`POST /api/v1/companies/{companyId}/financials/invoices`** — create an
  invoice; the server mints the id and invoice number; optional links (event
  **or** policy, plus payee). *Replaces the legacy create contract at this
  path for enabled companies.*
* **`PUT /api/v1/companies/{companyId}/financials/invoices/{invoiceId}`** —
  full document replacement (links excluded).
* **`DELETE /api/v1/companies/{companyId}/financials/invoices/{invoiceId}`** —
  terminal delete; live payments swept in the same action.
* **`POST …/invoices/{invoiceId}/relink`** — attach/detach/move the
  event/policy link (zero ledger rows).
* **`POST …/invoices/{invoiceId}/payee`** — set/clear/change the payee.
* **`POST …/invoices/{invoiceId}/payments`** — record a payment; the server
  mints `paymentId` and computes allocations pro-rata.
* **`DELETE …/invoices/{invoiceId}/payments/{paymentId}`** — remove a
  payment; the horizon rule's companion reserve unwind composes
  automatically.
* **`POST …/invoices/{invoiceId}/void`** and **`POST
  …/invoices/{invoiceId}/restore`** — write the charges down to zero and
  back.

Two reserve writes and two compositions:

* **`PUT …/events/{eventId}/reserves/{categoryId}`** — set the scope's
  absolute expected total.
* **`POST …/events/{eventId}/reserves/{categoryId}/history-reset`** — zero
  the scope's remaining expectation and mark the reserve feed's horizon.
* **`POST …/events/{eventId}/import`** — wholesale event refresh in one
  transaction (sweep → create → set reserves), idempotent by client-authored
  action ids.
* **`POST …/payees/{payeeId}/merge`** — repoint every linked invoice at
  another payee, re-recording payments; idempotent by convergence.

#### Changed: `POST /financials/validation/sync` contract

For enabled companies the read-state rebuild now takes an optional
`scope=invoices|balances` body (omitted = rebuild everything), runs under an
exclusive company-level lock, and returns per-table rebuilt counts
(`rebuilt.invoices` / `rebuilt.invoicePayments` /
`rebuilt.entityAccountBalances`). The previous `offset`/`limit`
window-walking shape is retired with the legacy surface, and the endpoint now
requires `company.payment:update` (previously `company.financials:sync`).

#### Deprecated: legacy financials endpoints

Deprecated, still serving companies not yet on the new surface; they will be
removed after the migration completes:

* **`POST /financials/event-transactions/import`** → use
  `POST /financials/events/{eventId}/import`.
* **`GET /financials/invoice-types`** → use
  `GET /financials/config/categories`.
* **`POST /financials/payees/reassign`** → use
  `POST /financials/payees/{payeeId}/merge`.

***

### 2026-07-21

#### Added: Financials read endpoints

Six new read endpoints expose the company's financial state (integer-cent
amounts, ISO dates):

* **`GET /api/v1/companies/{companyId}/financials/invoices`** — global and
  entity-scoped invoice lists in one endpoint: filter by status, category,
  linked event/policy/payee, invoice number, and date ranges
  (`unlinked=true` for invoices with no event and no policy link);
  cursor-paginated. Deleted invoices are hidden. Every row carries
  `headJournalId`.
* **`GET /api/v1/companies/{companyId}/financials/invoices/{invoiceId}`** —
  the whole current invoice document, its live payments, and
  `headJournalId` — the concurrency token for subsequent writes. Unlike the
  listing, returns deleted invoices.
* **`GET /api/v1/companies/{companyId}/financials/balances`** — per-category
  rollups (reserves / owed / paid / expected total) with per-account detail
  and account metadata embedded; raw signed cents with `normalBalance` so
  clients orient displays.
* **`GET /api/v1/companies/{companyId}/financials/entities/{entityType}/{entityId}/balances`** —
  one entity's balance slice (`entityType` is `event` | `policy` | `payee`)
  in the same category-grouped shape; zero and absent are the same state.
* **`GET /api/v1/companies/{companyId}/financials/events/{eventId}/reserve-updates`** —
  an event's reserve-update feed: user expected-total updates, automatic
  eroding-payment rows, and history-reset markers, newest first; optional
  `categoryId` filter.
* **`GET /api/v1/companies/{companyId}/financials/config/categories`** —
  read-only discovery of transaction categories and their line item types
  (the ids write payloads cite); `includeDeprecated=true` to resolve old
  references.

All six require `company.payment:read`.

#### Changed: `GET /financials/validation/check` contract

The read-state consistency check now takes `scope=invoices|balances` plus an
optional `updatedAfter`/`updatedBefore` window (bounding checked rows by
update time) and returns the list of stored-vs-recomputed `mismatches` — an
empty list means the scoped window is consistent. The previous
`offset`/`limit` transaction-window walk and per-subsystem report shape are
retired, and the endpoint now requires `company.payment:read` (previously
`company.financials:validate`).

#### Added: `displayAuthor` on policy transactions

Policy transactions now support an optional user-visible author label,
mirroring the existing `displayAuthor` on notes and file uploads. When an
integration or importer supplies it, the policy history displays the label
(e.g. "Data Import") in place of the acting user's name; audit attribution
(`createdBy`) stays server-set and unchanged.

* **`POST /api/v1/companies/{companyId}/policies/transaction/new-business`**
  and **`POST /api/v1/companies/{companyId}/policies/{policyId}/transaction/endorse`**
  accept an optional `displayAuthor` string (trimmed, non-empty, ≤ 255 chars).
* **`PATCH /api/v1/companies/{companyId}/policies/{policyId}/transactions/{transactionId}`**
  (new endpoint) sets the label on an existing transaction — display metadata
  only; no other transaction field can be modified. Requires `policy:update`.
* Transaction read responses (get/list) now include `displayAuthor`
  (`null` unless a label was supplied).

***

### 2026-07-17

#### Added: Rate a saved quote by id (stateless)

**`POST /api/v1/companies/{companyId}/quotes/{quoteId}/rate`** rates an
already-saved quote, named by id, and returns the rating results **without
persisting anything** — the by-id sibling of the full-body
`POST /api/v1/companies/{companyId}/quotes/rate`. The quote is never modified
(no field write, no rating run recorded; the quote row is byte-identical before
and after). The request body carries only `ratingWorkflowName`; the quote id is
a path parameter. There are deliberately no `data` overrides — to rate what-if
values, use the full-body endpoint. The saved quote's stored field bag runs
through the identical create-quote validation, exposure `id` hydration, and
rating pipeline the full-body endpoint uses, and the `200` response returns
`{ data }` — the quote's bag enriched with rating outputs. Any saved quote rates
regardless of `quoteStatus` (`bound` / `cancelled` included). `ratingWorkflowName`
is required in practice (missing/unknown → `400` listing the configured names;
no workflows configured → `422`); an unknown / deleted / other-company /
non-quote id returns a `404`; and a saved quote whose stored data no longer
validates against the current field configuration returns the same structured
`400` the full-body endpoint returns. Reuses the `company.quote:rate` permission.

#### Clarified: Hosted rating is stateless

The rating overview previously said
hosted rating "writes results to the rating response fields" and offered "a
single API call to rate and store results." Hosted rating never persists: both
rate endpoints return the enriched field bag and record nothing. Keeping the
results is a separate update the caller makes with the entity-update endpoints.
The documentation has been corrected to match the endpoints' actual behavior.

#### Changed: Legacy smart-tag names are no longer accepted

**`POST /api/v1/companies/{companyId}/forms/template`** and
**`PUT /api/v1/companies/{companyId}/forms/template/{number}`** now reject a
DOCX whose anchors carry the retired legacy smart-tag naming
(`AIIFmv1<ReferenceId>`, e.g. `AIIFmv1NamedInsured`). Field smart tags are
identified exclusively by their hashed form (`AIIFmv1Fld` + 16 hex characters,
e.g. `AIIFmv1Fld9E5D74B94AC85D40`). The rejection is a structured `400` with a
per-tag error that names the exact hashed replacement, so a rejected upload is
directly actionable. Already-stored templates and generated forms are
unaffected — every stored document was converged to hashed identities by the
fleet-wide touch migration before this change. To fix a legacy-tagged source
document, rename each anchor to the hashed identity the error names, or
re-insert the tags from the form editor's smart-tag sidebar. Fixed non-field
tags (e.g. `AIIFmv1CurrentDate`) keep their readable names and are
unaffected.

***

### 2026-07-16

#### Added: Bind Quote — one call turns a quote into a policy

**`POST /api/v1/companies/{companyId}/quotes/{quoteId}/bind`** binds a quote into
a policy in a single, atomic call — the one-call convenience over hand-rolling the
equivalent policy transaction from the quote's data. There is **no request body**:
the quote is read server-side, and on success the quote is linked to the resulting
policy and the policy transaction is committed together, so a quote can never end
up bound to a policy that was not created (or vice versa).

* **All five quote types bind through this one endpoint.** The endpoint dispatches
  on the quote's type: `newBusiness` and `renewal` mint a brand-new policy (a
  renewal's new policy is linked back to the expiring term), while `endorsement`,
  `cancellation`, and `reinstatement` transact against their existing source policy
  in place. The response `policyId` is the policy the transaction landed on — the
  new policy for `newBusiness` / `renewal`, the existing source policy for the
  in-place types.
* **Response:** `201 { policyId }`.
* **Already bound:** a quote that is already `bound` is rejected with **`409`**,
  and the error body carries **`referencingPolicy`** (the id of the policy it is
  already bound to) so you can recover it without another call.
* **Validation:** the quote must satisfy the same policy-create rules a
  `new-business` transaction enforces — notably the primary-insured identity on
  `fullTermPolicyInfo` (`primaryInsuredJoin`, referencing an existing Exposure,
  plus `primaryInsuredName`). A quote that fails them returns a structured `400`.
* **Required permission:** `company.policy:create` — a bind is a policy create.

***

### 2026-07-15

#### Changed: Stateless Quote Rating hydrates exposure id references

**`POST /api/v1/companies/{companyId}/quotes/rate`** now looks up and merges the
stored data for exposures referenced by `id`. Because embedded exposures are
referenced by `id` over the API (you cannot inline-create an exposure), each
referenced Exposure's stored fields are fetched and merged under the reference
before rating, so the exposure is rated against its real stored data rather than
a blank record. Fields supplied inline win over the stored values (the body is a
draft-edit over the stored exposure); the lookup is read-only and nothing is
persisted. Additionally, a body whose required rating target (e.g.
`quote.exposures`) is empty or missing on the quote after this merge now returns
a structured `400` naming the workflow and target path, instead of an opaque
`500`.

***

### 2026-07-14

#### Added: Stateless Quote Rating

**`POST /api/v1/companies/{companyId}/quotes/rate`** rates the quote a
create-quote body would create and returns the results — **without persisting
anything** (no quote row, no rating run). The request body carries the exact
create-quote payload under `data` plus a `ratingWorkflowName`; the `data` bag is
validated with the identical create-quote pipeline (a body create-quote would
reject fails with the same structured `400`), the named workflow runs, and the
response echoes `{ data }` enriched with rating outputs. `ratingWorkflowName` is
required — a missing/unknown name returns a `400` listing the configured names,
and a company with no rating workflows returns a `422`. Establishes the external
`quotes/` namespace. Requires the new `company.quote:rate` permission.

***

### 2026-07-13

#### Added: Touch Form Template and Touch Generated Form

**`POST /api/v1/companies/{companyId}/forms/template/{number}/touch`** and
**`POST /api/v1/companies/{companyId}/forms/generated/{id}/touch`** bring a
form up to date in place: legacy-style smart-tag anchors
(`AIIFmv1<ReferenceId>`) are converged to the current hashed identity format
(`AIIFmv1Fld…`), and a generated form's cached smart-tag metadata is refreshed
from the converged document. Content, substituted values, and tag display names
are untouched, and a template touch does not create a new version. Both
endpoints are idempotent — an already-converged form reports `changed: false`
and writes nothing — and respond with the per-form outcome (`changed`,
`renamedTags`, and for generated forms `metadataRefreshed`). Requires the
`forms:update` permission.

#### Added: List Generated Forms

**`GET /api/v1/companies/{companyId}/forms/generated`** lists a company's
**generated forms** — instances produced from form templates against specific
records — which were previously not enumerable over the API (the existing
`GET …/forms` lists the template library). Paginated
(`?page=`/`?pageSize=`, with a `totalCount`), optionally filtered by
`?category=`. Each item carries `id`, `name`, `category`, `templateId`,
`bound` (whether a finalized copy exists alongside the editable draft), and
timestamps. Requires the `forms:read` permission.

***

### 2026-07-10

#### Added: Back-datable `displayDate` on Notes and Files

Historical imports and integrations can now supply an optional
**`displayDate`** (ISO 8601 timestamp) when creating records, so imported
history displays and sorts under its original date instead of the import
time. Audit timestamps (`createdAt`/`updatedAt`) and creator/uploader
attribution remain server-set and cannot be overridden.

* **`POST /api/v1/companies/{companyId}/notes`** accepts an optional
  `displayDate`; note responses expose it (`null` when unset).
  **`GET …/notes`** now orders by the display date (`displayDate` when set,
  else `createdAt`), so backdated notes interleave correctly.
* **`POST /api/v1/companies/{companyId}/files`** (upload intent) accepts an
  optional `displayDate`; file list items and file metadata expose it.
  **`GET …/files`** now orders by it the same way.

***

### 2026-07-09

#### Added: Explicit `modules` selection and `replace` guard on Seed Configuration

**`POST /api/v1/companies/{companyId}/configuration/seed`** gains two request
fields, and its discovery endpoint now advertises the starter-module catalog.

* **`modules`** — an explicit starter-module selection (checkbox granularity).
  Mutually exclusive with `starterSheet`: `modules` is authoritative for every
  axis and feature (the server unions it with the always-on base `core` +
  `default` but appends no default feature modules), whereas `starterSheet`
  expands to a complete out-of-box config. Supplying both is a `400`
  (`InvalidModuleSelection`); an unknown module id is a `400`
  (`UnknownStarterModule`); selecting two modules from the same pick-one axis —
  e.g. two raters or two exposure schemes — is a `400` (`InvalidModuleSelection`).
* **`replace`** — an overwrite guard. Seeding a company that already has a
  non-empty configuration is now refused with a **`409` (`ConfigAlreadyExists`)**
  unless `replace: true`; a never-configured company still seeds without it.
* **`GET /api/v1/companies/{companyId}/configuration/seed/options`** now returns
  a `modules` array — the full starter-module catalog, each entry carrying `id`,
  `label`, `description`, `defaultSelected`, its axis `group` (`always-on` /
  `exposure-scheme` / `rating` / `policy-number` / `feature`), and `alwaysOn` —
  alongside the existing `starterSheet` `options`.

`POST /api/v1/companies/{companyId}/configuration/seed/generate` is unchanged: it
still accepts `starterSheet` only (no `modules`, no `replace`), since it never
mutates the company.

***

### 2026-07-07

#### Breaking: Bordereau `additionalColumns` removed — `columns` is the one column-selection mechanism

The legacy `additionalColumns` parameter is **removed** from all three
bordereau endpoints (**`GET /api/v1/companies/{companyId}/policies/bordereau`**,
**`GET …/bordereau/download`**, **`POST …/bordereau/export`**). Field columns
are now requested exclusively via `columns` `{"kind":"field"}` entries (each a
`path` into the policy's field data plus a display `header`); the per-row
resolution semantics are unchanged.

* **Migration:** replace each `{"path":"…","columnHeader":"…"}` entry with a
  `columns` entry `{"kind":"field","path":"…","header":"…"}` (on the rendered
  surfaces, list the fixed columns you want alongside them — `columns` is the
  complete ordered set there). A request still sending the removed parameter is
  not rejected: unknown parameters are ignored, so its extra columns silently
  no longer appear in the output.
* **The JSON list endpoint now accepts `columns`, restricted to
  `{"kind":"field"}` entries.** Column order/omission is meaningless in its
  typed JSON rows (every fixed property is always present), but field
  *selection* is not — the selected fields resolve into each row's map. A
  `{"kind":"fixed"}` entry there is rejected with HTTP 400 and a clear message.
  (This supersedes the initial `columns` design note that the list endpoint
  took no `columns` at all.)
* **Response rename:** the per-row output map on the list endpoint is renamed
  `additionalColumns` → **`fieldColumns`** (same shape: resolved values keyed
  by the requested headers; empty object when no field columns are requested).
* The `columns`-vs-legacy **mutual-exclusion 400 is gone** — with one
  mechanism left there is nothing to be exclusive with.

This ships without a deprecation window: the API has no live third-party
consumers yet, so the surface was unified before first external adoption.

#### Added: Bordereau full column selection via `columns` (download + export)

The bordereau rendered surfaces — **`GET
/api/v1/companies/{companyId}/policies/bordereau/download`** (CSV) and **`POST
…/policies/bordereau/export`** (Google Sheets) — accept a new optional
`columns` parameter: an ordered array of column specs that **completely**
describes the output. The 12 fixed columns become selectable, omittable, and
reorderable (`{"kind":"fixed","key":"policyNumber"}`), and field columns
(`{"kind":"field","path":"policyStatus","header":"Status"}`) interleave
anywhere. On the CSV download `columns` is a JSON-encoded query param; on the
export it is a JSON array in the POST body.

* Omitting `columns` preserves the default output — the 12 fixed columns in
  canonical order.
* The JSON list endpoint's `columns` support (field-only entries) is described
  in the entry above.

#### Changed: Bordereau field columns accept any field path, resolved at each transaction's effective date

The bordereau endpoints (**`GET /api/v1/companies/{companyId}/policies/bordereau`**,
**`GET …/bordereau/download`**, **`POST …/bordereau/export`**) now accept **any**
dot-path in a field column — previously a path had to begin with one of the
policy-root FullTerm containers (`fullTermPolicyInfo.`,
`fullTermPolicyBillingInfo.`, `fullTermPolicyRatingResult.`) and anything else
was rejected with HTTP 400.

* **Per-row resolution moved to the transaction's effective date.** Each row now
  resolves its field-column paths against the policy data as of that
  transaction's effective date, so per-segment fields (e.g. `policyStatus`, or a
  mid-term endorsement's changed values) report the value the transaction put in
  force. Existing FullTerm and fixed columns are unchanged — their values are
  identical across the policy term by construction.
* **Malformed dot-paths** (empty, or leading/trailing/doubled dots) are still
  rejected with HTTP 400; a well-formed path that doesn't exist in the policy
  data yields an **empty value** instead of an error.
* **Object-typed leaves render as values, not `[object Object]`** — Date,
  Address, and Currency fields are formatted; other objects serialize as JSON.

***

### 2026-07-02

#### Added: List Forms and Download Form (read endpoints for the Forms API)

Two new read endpoints complete CRUD on the Forms API, both requiring the
`forms:read` permission:

* **`GET /api/v1/companies/{companyId}/forms`** — list a company's form library
  (one summary per form's current version), paginated (`page`/`pageSize`, with a
  `totalCount`) and optionally filtered by `category` and/or `kind`. Each item
  includes `number`, `name`, `kind` (`template` vs `static`), `category`,
  `templateKey`, `version`, and timestamps.
* **`GET /api/v1/companies/{companyId}/forms/{number}`** — download a form's
  current-version original file (the DOCX for templates, the PDF for static
  forms) by its `FM-XXXX` number. Returns `{ downloadUrl, fileName,
  contentType, expiresAt }`, where `downloadUrl` is a 15-minute signed URL the
  consumer GETs the bytes from directly.

Together these enable round-tripping (export a form, edit it, re-upload via the
replace endpoints) and config-as-code backup of a company's form library. The
change is purely additive — no existing endpoint changes.

***

### 2026-07-01

#### Changed (additive): List Parse Runs now reports logical runs with per-stage detail

**`GET /api/v1/companies/{companyId}/files/parse-runs`** now reports one record
per **logical parse run** — the `extract` and `create_<flow>_v<N>` stages of a
single trigger, grouped — instead of one flat record per pipeline task. All
existing fields keep their types and documented semantics; the change is
additive:

* `target` is now populated with the `{ kind, id }` file or folder the run
  parsed (previously always `null`). Runs that predate grouping still report
  `target: null` and are tagged `legacy: true`.
* New optional fields per record: `stages` (per-stage `status` — including the
  distinct `retrying` and `pending` — plus `attempts`, `error`, `finishedAt`),
  `createdEntities` (`entityType` / `entityId` / `deleted` for every entity the
  run created), `extractionReady`, `rerunOfRunId`, `reusedExtractOfRunId`, and
  `legacy`.
* `status` is unchanged: still exactly `running` / `succeeded` / `failed`, with
  every internal retry state reading as `failed` (per-stage `retrying` detail
  now lives in `stages`).

#### Breaking: Upload Form Template moved to `/forms/template`

**Upload Form Template** has moved from
**`POST /api/v1/companies/{companyId}/form-templates`** to
**`POST /api/v1/companies/{companyId}/forms/template`**. The request/response
contract, `forms:create` permission, and DOCX-only smart-tag validation are
unchanged — only the path changed, grouping form uploads under a `forms/`
namespace alongside the new static-form endpoint. Update any integration that
posts to the old path.

#### Added: Upload Static Form (PDF)

* **Added: Upload Static Form** — **`POST /api/v1/companies/{companyId}/forms/static`**
  uploads a `.pdf` **static form** (a certificate, handout, or any finished PDF)
  into a company instance under a chosen `category` (`event`, `quote-flow`, or
  `quote-bind-flow`). PDFs carry no smart tags, so nothing is validated — the
  file is committed as-is and forms generated from it are the PDF unchanged.
  PDF only; the file is sent as base64. Complements **Upload Form Template**
  (DOCX + smart tags). **Required permission:** `forms:create`.

#### Added: Replace a form in place (type-specific)

* **Added: Replace Form Template** — **`PUT /api/v1/companies/{companyId}/forms/template/{number}`**
  replaces a **DOCX template's** content in place, and **Replace Static Form** —
  **`PUT /api/v1/companies/{companyId}/forms/static/{number}`** replaces a **static
  PDF form's** content. The uploaded file becomes a new version under the SAME
  identity — same `number`, `templateKey`, display name, and category — so
  forms-logic rules and existing form bindings that reference the number keep
  working (no need to keep minting new forms). The template endpoint validates
  embedded smart tags against the template's existing category (DOCX-only); the
  static endpoint commits the PDF as-is (no tags). Both are **same-type only** —
  a replace can never flip a form's file type (a DOCX↔PDF mismatch returns `400`
  `form-template-type-mismatch` / `form-static-type-mismatch`), and `category` is
  not part of the request (a replace never re-scopes). An unknown number returns
  `404`. **Required permission:** `forms:update`.

#### Added: Delete Form

* **Added: Delete Form** — **`DELETE /api/v1/companies/{companyId}/forms/{number}`**
  soft-deletes a form (DOCX template or static PDF) addressed by its stable
  `FM-XXXX` `number` (the value returned by the upload endpoints). Every version
  is removed from the library, but forms already generated from it keep working
  (they resolve the version they were bound to). Forms-logic rules are
  configuration and are left untouched. An unknown or already-deleted number
  returns `404` (`form-template-not-found`) rather than a silent success.
  **Required permission:** `forms:delete`.

***

### 2026-06-30

#### Breaking: Finalize Upload no longer accepts `flow` / `parseVersionOverride`

The opt-in parse trigger has been removed from **Finalize Upload**
(**`POST /api/v1/companies/{companyId}/files/{fileId}/finalize`**). The endpoint
is now a pure `pending → ready` flip and accepts only `versionId`; the
`flow` and `parseVersionOverride` body fields are gone.

* Parsing is now reachable **only** through **Trigger Parse**
  (**`POST /api/v1/companies/{companyId}/files/trigger-parse`**). To parse on
  upload, finalize the file and then POST it to trigger-parse.

#### Added: List Parse Runs

* **Added: List Parse Runs** — **`GET /api/v1/companies/{companyId}/files/parse-runs`**
  lists a company's parse runs, newest-first and paginated (1-based
  `page` / `pageSize`), so you can poll the outcome of a parse you kicked with
  **Trigger Parse**. Each pipeline task (`extract`, `create_<flow>_v<N>`) is its
  own flat run record with a `runId`, `flow` (`null` for the flow-agnostic
  `extract` stage), `status` (`running` / `succeeded` / `failed` — the internal
  `failed_*` retry states collapse to `failed`), `attempts`, optional `error`,
  and timestamps. `target` is currently always `null`. **Required permission:**
  `company.file:read`.

***

### 2026-06-29

#### Breaking: dropped the `external` segment from every API path (`/api/v1/external/*` → `/api/v1/*`)

Every endpoint has moved off the `/api/v1/external/` prefix onto `/api/v1/` — the `external` URL segment is gone, and the `v1` version segment is unchanged. There are **no backwards-compatible aliases**: requests to the old `/api/v1/external/*` paths now return `404`. Consumers must update their base path.

* The base origin is unchanged (`https://go.aiinsurance.io`). Only the path prefix changed.
* Example: `POST /api/v1/external/companies/{companyId}/files` → **`POST /api/v1/companies/{companyId}/files`**.
* Likewise `GET /api/v1/external/me/companies` → **`GET /api/v1/me/companies`**, and so on for every route.
* Request/response shapes, `operationId`s, permissions, and behavior are otherwise unchanged — only the path prefix moved. Update any saved URLs, base-path configuration, and regenerate SDKs.

#### Added: upload a form template with smart tags

* **Added: Upload Form Template** — **`POST /api/v1/companies/{companyId}/form-templates`** uploads a `.docx` form template into a company instance under a chosen `category` (`event`, `quote-flow`, or `quote-bind-flow`). The DOCX's embedded smart tags are validated against the catalog for that category *before* the template is committed: any unknown, not-enabled, wrong-category, or unsupported tag rejects the whole upload with a `400` whose `details` name each offending tag. DOCX only; the file is sent as base64. Built to support migrating forms between instances with their smart-tag links intact. **Required permission:** `forms:create`.

***

### 2026-06-26

#### Breaking: dropped the `-json` suffix from the configuration endpoints

Now that the spreadsheet configuration endpoints are gone and JSON is the only machine format, the `-json` suffix is redundant. The four endpoints have been renamed to their clean paths, and their `operationId`s renamed to match. There are **no backwards-compatible aliases** — the old `-json` paths now return `404`. Update any saved URLs and regenerate SDKs.

* `POST /configuration/import-json` → **`POST /configuration/import`** (`operationId` `importFmv1ConfigurationJson` → `importFmv1Configuration`)
* `POST /configuration/export-json` → **`POST /configuration/export`** (`operationId` `exportFmv1ConfigurationJson` → `exportFmv1Configuration`)
* `POST /configuration/compare-json` → **`POST /configuration/compare`** (`operationId` `compareFmv1ConfigurationJson` → `compareFmv1Configuration`)
* `POST /configuration/validate-json` → **`POST /configuration/validate`** (`operationId` `validateFmv1ConfigurationJson` → `validateFmv1Configuration`)

The request/response shapes, permissions, and behavior are unchanged — only the paths and `operationId`s changed.

#### Breaking: removed the spreadsheet Import and Export endpoints

The two Google-Spreadsheet configuration endpoints have been removed from the external API. Their JSON equivalents are the canonical machine surface and fully cover this functionality.

* **Breaking: removed `POST /api/v1/external/companies/{companyId}/configuration/import`** — applied changes from a Google Spreadsheet to the database. Use **`POST /configuration/import-json`** (apply a structured JSON configuration body) for the Google-free, JSON-native equivalent.
* **Breaking: removed `POST /api/v1/external/companies/{companyId}/configuration/export`** — wrote current config to an existing Google Spreadsheet. Use **`POST /configuration/export-json`** (returns current config as a structured JSON body) instead; its output is the exact shape `import-json` accepts, so `export-json` → edit → `import-json` is a lossless round-trip.
* **Unaffected:** the in-app onboarding spreadsheet import/export UI. The `company.configuration:import` / `:export` permissions are unchanged (still used by the JSON endpoints and the seed surface).

#### Restructured the seed surface; removed the spreadsheet Generate endpoint

The seed configuration surface is now machine-discoverable and Google-free, and the old Google-Spreadsheet **Generate** endpoint has been removed.

* **Added: List Seed Options** — **`GET /api/v1/external/companies/{companyId}/configuration/seed/options`** returns the valid `starterSheet` variants, each with a description and a default marker, plus the `defaultStarterSheet` applied when `starterSheet` is omitted. Sourced from a single in-code registry, so it cannot drift from the names the seed endpoints accept. **Required permission:** `company.configuration:export`.
* **Added: Generate Seed Configuration (JSON)** — **`POST /api/v1/external/companies/{companyId}/configuration/seed/generate`** returns the exact JSON configuration a seed *would* apply for the selected `starterSheet` — **without seeding, without mutating the company, and without Google**. The body is the shape **import-json** accepts, so you can review/edit it and POST it to **import-json**. Optional `{ starterSheet?: string }`; no `googleOAuthToken`. **Required permission:** `company.configuration:export`.
* **Breaking: removed `POST /api/v1/external/companies/{companyId}/configuration/generate`** — the endpoint that created a blank **Google Spreadsheet** template has been removed from the external API. Use **Generate Seed Configuration (JSON)** for the Google-free, JSON-native equivalent. (The in-app spreadsheet generation UI is unaffected.)
* **`POST /configuration/seed` is unchanged** — it still applies starter content directly to the company. Its `starterSheet` field now also documents an `enum` of the valid variant names.

#### Documentation: object-primitive corrections (no API change)

Several object-primitive docs were corrected to match the implementation. **No runtime behavior changed** — these are documentation fixes.

* **`CoverageLimit` was never a built-in object primitive.** The docs (and the `Fmv1CoverageLimit` OpenAPI schema) described a `CoverageLimit` primitive with required `{ coverageLimitName, coverageLimitAmount }`. No such primitive exists at runtime. The thing tenants actually model is a **custom object** named `NumberLimit` (`{ numberLimitName, numberLimitAmount }`), defined in the (default) tenant configuration — `Coverage.coverageLimits` is a list of it. The schema was renamed `Fmv1CoverageLimit` → **`Fmv1NumberLimit`** with the real keys and is now documented as a custom-object example, not a built-in primitive.
* **The built-in object primitives are exactly `Address`, `Currency`, `Date`, and `StringOrNumber`.** Earlier entries listed `CoverageLimit` (never real) and `QuoteBindError` (since removed), and omitted `StringOrNumber`. The [Object Primitives reference](/api-reference/object-primitives/overview) is now accurate.
* **Object-primitive requiredness is per-primitive, not blanket.** The previous "every sub-field is required" rule was only ever true for `Currency` (`value`), `Date` (`day`/`month`/`year`), and `StringOrNumber` (`kind`). **`Address` sub-fields are all optional** — a partial address is valid and is not rejected. `timezone` on a `Date` is optional in the DMY shape.
* **Date input shapes documented.** A generic `Date` field accepts either `{ day, month, year, timezone }` (timezone optional) or the ISO envelope `{ date: "YYYY-MM-DD", timezone }` (timezone required) on **every** write path, entity CRUD and policy transactions alike. Policy term dates (`fullTermPolicyInfo.policyStartDate`/`policyEndDate`) are the exception: they accept a full ISO 8601 string with a UTC offset, or a structured `{ year, month, day, timezone }` object — but **not** the `{ date, timezone }` envelope.
* **New-business example clarified.** The policy `new-business` example mixes framework fields (`primaryInsuredJoin`, `primaryInsuredName`, `fullTermPolicyInfo` dates) with default-tenant-config-specific fields (`annualPremium`, `primaryInsured`, `bedCount`, …). The example now flags which fields are config-specific.

***

### 2026-06-25

#### Added: Seed Configuration endpoint (Google-free config setup)

New endpoint **`POST /api/v1/external/companies/{companyId}/configuration/seed`** puts a company into a usable FMV1 configuration state directly from the framework's **code-defined starter content** — with **no Google Spreadsheet and no Google OAuth token**. It is the Google-free counterpart to `POST /configuration/import`: where import reads a spreadsheet, seed materializes the equivalent starter content in-memory and runs it through the same validate → compare → apply pipeline.

* **Request:** optional `{ starterSheet?: string }` — which starter module variant to seed (omitted → the product default). A bodyless `POST` seeds the product default.
* **Response:** `{ success: true, message? }` on success; a `400` with an error (no changes applied) if the starter content fails validation.
* **Required permission:** `company.configuration:import` (same as Import; **FMV1\_CONFIGURATION\_MANAGER** role).

***

### 2026-06-17

#### Breaking: by-id update endpoints are PATCH-only (PUT removed)

The by-id update endpoints accepted **both `PUT` and `PATCH`**, both performing a partial merge. Because a partial merge is `PATCH` semantics — and `PUT` implies full replacement, which these endpoints do **not** do — accepting `PUT` was misleading. `PUT` has been removed: a `PUT` to any of these routes now returns **`405 Method Not Allowed`** with an **`Allow: PATCH`** response header. `PATCH` behaviour is unchanged (only provided fields change, an explicit `null` clears, omitted keys are untouched).

**Affected endpoints** (all under `/api/v1/external/companies/{companyId}`):

* `PATCH /entities/{entityType}/{entityId}`
* `PATCH /notes/{noteId}`
* `PATCH /tasks/{taskId}`
* `PATCH /files/{fileId}`
* `PATCH /folders/{folderId}`
* `PATCH /files/{fileId}/placements/{placementId}`

**Migration:** if you were sending `PUT` to any of these, switch the verb to `PATCH` — the request body and semantics are identical (they were always a partial merge). `PUT` was previously documented as a `200` alias of `PATCH` on the file, folder, task, note, and placement routes; that alias is removed.

***

### 2026-06-16

#### Documentation: accuracy reset across the API reference

The API reference was audited end-to-end against the live route surface and corrected so every documented endpoint, schema, status code, and permission matches what the API does today. No runtime behaviour changed — these are documentation corrections only.

* **Removed phantom endpoint groups.** Three capability groups that had never shipped as endpoints (`resolve-address` / Address Tools, Event Financials, and quote send) were removed from the spec; they were never callable.
* **Corrected every endpoint's schema, examples, status codes, and permissions** to match the implementation, including the **bare (un-prefixed) permission strings** that the authorization guard actually checks: `insured:update` / `insured:delete` for the Exposure update/delete, `policy:update` for the policy cancel/endorse/reinstate transactions, and `policy:delete` for transaction delete. Create/read operations keep their `company.`-prefixed permissions. The Event configuration endpoint requires `company.event:export`.
* **Raw `Authorization` header.** External API authentication takes your API key as the **raw** `Authorization` header value with **no scheme prefix** — not `Bearer ` and not `ApiKey `, and never `X-API-Key`. See [Generating API Keys](/api-reference/authentication).
* **Narrative pages reconciled.** The overview, roadmap, getting-started, object-primitives, and data-models pages were corrected: Submissions, Persons/Organizations, Notes, Tasks, and Company Files are documented as **available today** (not "planned"); the unified entity envelope (`fieldModelV1Data` with epoch-second `createdAt`/`updatedAt`, no top-level `companyId`) and its `{ items, hasMore, totalCount }` / zero-based `pageNumber` list shape are documented accurately; and broken internal links were repaired.

***

### 2026-06-11

#### Added: per-placement organize — move/categorize a shared file under one entity

`folderId` and `category` are per-placement attributes, so the owner-less `PATCH /files/{fileId}` cannot address them once a file is shared (its `409 Conflict` below). The new placement update lifts that limitation:

* `PATCH /api/v1/external/companies/{companyId}/files/{fileId}/placements/{placementId}` — update ONE placement's `folderId` (a folder of the placement's owner, or `null` for the owner's top level) and/or `category` (free-text ≤255 chars, or `null` to clear). Absent fields are untouched; at least one must be present. Returns `200 { placementId, fileId, entityType, entityId, folderId, category }`; the file's other placements are never affected. Placement ids come from `GET /files/{fileId}/placements` or the share response. Requires `company.file:update`.
* The `409 Conflict` body returned by `PATCH /files/{fileId}` for `folderId`/`category` on a shared file now points at the placement endpoint: `File has multiple placements: {fileId}. Update one placement instead: PATCH /files/{fileId}/placements/{placementId} (enumerate them with GET /files/{fileId}/placements)`. Single-placement files are unaffected — either endpoint works there.

#### Added: file placements — share one file across entities

A file can now be placed on more than one entity at a time. Sharing adds a **placement** — never a copy: the bytes are stored once and every placement sees the same current version and history. Folder location and `category` are per placement; `displayName` stays on the file. Sharing is same-company only.

* `GET /api/v1/external/companies/{companyId}/files/{fileId}/placements` — list everywhere a file appears (`[{ placementId, entityType, entityId, entityDisplayName, folderId, category, createdAt }]`). Requires `company.file:read`.
* `POST /api/v1/external/companies/{companyId}/files/{fileId}/placements` — share the file to another owner (`entityType`, `entityId?`, optional `folderId` of the target owner). Returns `201 { placementId, fileId, entityType, entityId, folderId }`; a duplicate share to the same owner is `409 Conflict`. Requires `company.file:create`.
* `DELETE /api/v1/external/companies/{companyId}/files/{fileId}/placements/{placementId}` — remove the file from ONE owner. While other placements remain, the file and its content are untouched; removing the **last** placement deletes the file and reclaims storage (`fileDeleted: true`). Requires `company.file:delete`.
* The file responses now carry placement information: list items gained `placementId` and `placementCount`; `GET /files/{fileId}` gained `placementCount`, and its owner fields (`entityType`/`entityId`/`folderId`/`category`) now describe the file's *primary* (oldest) placement.
* `PATCH /files/{fileId}` on a **shared** file can only change `displayName`; an owner-less `folderId`/`category` update is ambiguous across placements and returns `409 Conflict`. Single-placement files behave exactly as before.
* `DELETE /files/{fileId}` removes the file **everywhere** (all placements) — unchanged for single-placement files; use the placements endpoint for per-entity removal.

***

### 2026-06-10

#### Added: file categories

Files can now carry a free-text `category` label (max 255 characters). There is no configured category list — any non-blank string is a valid category.

* `PATCH /api/v1/external/companies/{companyId}/files/{fileId}` accepts an optional `category` field alongside `displayName`/`folderId`: a string sets the label, an explicit `null` clears it, and an absent field leaves it untouched.
* The file responses (`GET /files/{fileId}` metadata and the `GET /files` list items) already include `category` (`null` when unset).

#### Breaking: Company Files API rebuilt on signed URLs

The Company Files API was rebuilt end-to-end. File bytes no longer travel through the API — uploads and downloads now go straight to cloud storage via short-lived signed URLs, and every request/response shape changed. There is no compatibility mode for the old contract. See the [Company Files overview](/api-reference/company-files/overview) for the new workflow.

* **Upload is now a two-phase handshake.** `POST /api/v1/external/companies/{companyId}/files` no longer accepts `multipart/form-data`; it takes a JSON upload intent (`entityType`, `entityId?`, `folderId?`, `fileName`, `contentType`, `byteSize`) and returns `201 { fileId, versionId, uploadUrl }`. PUT the bytes to `uploadUrl` (pinned to the declared content type and byte size, 15-minute expiry), then `POST /files/{fileId}/finalize` with the `versionId` to make the file visible.
* **Files and folders are now owner-scoped.** Every file/folder belongs to a configured entity (`entityType` + `entityId`) or the company level (`entityType: "company"`). The folder endpoints keep their five paths but now require the owner on create/tree and return new shapes; `GET /folders` returns the owner's whole tree as `{ folders: [{ id, parentFolderId, name }] }` (no pagination), and `GET /folders/{folderId}` returns `{ folder, folders, files, totalCount }` instead of the mixed `contentType`-discriminated item list.
* **`GET /files/{fileId}` returns the new metadata shape** — `displayName`/`fileName`/`contentType`/`byteSize`/`status`/`folderId`/`category`/`createdAt`/`updatedAt`/`uploadedAt`/`uploadedBy` replace the legacy `name`/`mimeType`/`entityName`/`userDate` fields.
* **`PATCH /files/{fileId}` renames with `displayName`** (was `name`) and/or moves with `folderId`; it returns `{ id }` (was the full file). `PATCH /folders/{folderId}` keeps `name`/`parentFolderId` but returns `{ id }`; reparenting is cycle-checked. `DELETE /folders/{folderId}` now reports the cascade: `{ id, deleted, deletedFolders, deletedFiles }`.

#### Removed: binary download and multipart upload

* `GET /api/v1/external/companies/{companyId}/files/{fileId}/content` (binary stream) is removed — use the new `GET /files/{fileId}/download-url`, which returns `{ url, expiresAt, fileName, contentType, byteSize }` (requires the new `company.file:download` permission), and fetch the bytes from the signed `url`.
* The `multipart/form-data` upload body is removed — see the upload handshake above.

#### Added: list files

* `GET /api/v1/external/companies/{companyId}/files` — list an owner's files (`entityType`, `entityId?`, optional `folderId` placement filter, 1-based `page`/`pageSize`). Previously files were only discoverable through folder contents. Requires `company.file:read`.
* `POST /api/v1/external/companies/{companyId}/files/{fileId}/finalize` and `GET /api/v1/external/companies/{companyId}/files/{fileId}/download-url` — the new halves of the signed-URL upload/download workflow.

***

### 2026-06-07

#### Removed: `QuoteBindError` object primitive

The `Object: QuoteBindError` object primitive (and its `Fmv1QuoteBindError` OpenAPI schema) has been removed. It was never produced at runtime — no endpoint ever returned or accepted a `quoteBindErrors` value — so this removal is not expected to affect any integration. The remaining built-in object primitives (`Address`, `Currency`, `Date`, `StringOrNumber`) are unchanged. See the [Object Primitives reference](/api-reference/object-primitives/overview). (This entry originally listed a `CoverageLimit` primitive; it was never a built-in primitive — see the 2026-06-26 correction below.)

***

### 2026-06-01

#### Added: Notes endpoints

Notes can now be managed via the external API. Notes are simple text records attached to a top-level Field Model V1 entity (`Event`, `Exposure`, `Quote`, `Submission`, `Person`, `Organization`, `Policy`). The parent entity type travels as the `entityType` query parameter on every verb; permission is gated by the parent entity (read for GET, update for write).

* `GET /api/v1/external/companies/{companyId}/notes` — List notes for a parent entity (1-based `page`/`pageSize`)
* `POST /api/v1/external/companies/{companyId}/notes` — Create a note (returns `{ id }`, HTTP 201)
* `GET /api/v1/external/companies/{companyId}/notes/{noteId}` — Get a note
* `PATCH /api/v1/external/companies/{companyId}/notes/{noteId}` — Update a note's body
* `DELETE /api/v1/external/companies/{companyId}/notes/{noteId}` — Soft-delete a note

#### New: Tasks API

Added 5 endpoints for managing company tasks at `/api/v1/external/companies/{companyId}/tasks`:

* `GET /tasks` — List tasks (paginated, filter by `status`, `assigneeId`, `entityType`, `entityId`). Requires `company.task:read`.
* `POST /tasks` — Create a task. Returns `{ id }`. Requires `company.task:create`.
* `GET /tasks/{taskId}` — Get a single task. Requires `company.task:read`.
* `PATCH /tasks/{taskId}` — Partial update (only changed fields; unknown fields rejected). Requires `company.task:update`.
* `DELETE /tasks/{taskId}` — Soft delete. Returns `{ id, deleted: true }`. Requires `company.task:delete`.

A task has a `name`, `description`, `status` (`Not Complete` / `Complete`), ISO 8601 `deadline`, optional linked `entity` snapshot (`{ type, id, name }`), and `assignees` (company user IDs — non-members are rejected with `400`). See the [Tasks overview](/api-reference/tasks/overview).

#### Breaking: Full-term policy transaction reshape

The segmented Policy Transaction API moved to the full-term ("Model-B") design. Affects `new-business`, `endorse`, `cancel`, `reinstate`, and `renew`.

* **Term bounds come solely from `fullTermPolicyInfo`.** `policyStartDate` / `policyEndDate` are read from `fieldModelV1Data.policy.fullTermPolicyInfo` on NEW\_BUSINESS / RENEW — the top-level `policyStartDate` / `policyEndDate` (and RENEW's top-level `previousPolicyId` / `newPolicyStartDate` / `newPolicyEndDate`) parameters are **removed**. `previousPolicyId` now lives in `fullTermPolicyInfo`.
* **`fullTermPolicyBilling` renamed to `fullTermPolicyBillingInfo`** in every request, response, and bordereau column path.
* **`policyStatus` is a segment-scoped policy field** with lowercase values `"active"` / `"cancelled"` — no longer inside `fullTermPolicyInfo`.
* **ENDORSE now has five channels:** `deltas` **XOR** `fullTermDeltas` (the latter restricted to `policy.fullTermPolicyInfo`, no dates), plus additive `fullTermPolicyBillingInfo`, `fullTermPolicyRatingResult`, and `crossSegmentRatingOutputs`. List elements are addressed by predicate — `exposures[id = '…']`.
* **CANCEL / REINSTATE** take the date plus optional whole-object `fullTermPolicyBillingInfo` / `fullTermPolicyRatingResult`. CANCEL flips per-segment `policyStatus` to `"cancelled"` and records a single `cancellationEffectiveOnDate` (uniform across the term); REINSTATE flips `policyStatus` back to `"active"` and clears `cancellationEffectiveOnDate` (no reinstatement date field). A reinstate that would leave a coverage gap is rejected — that scenario is a new policy term. `policyEarlyTerminationDate` is **removed**.
* **Rating output split** into `fullTermPolicyRatingResult` (policy-root, hoisted) and `crossSegmentRatingOutputs` (element-level, inline). Responses hoist `fullTermPolicyInfo`, `fullTermPolicyBillingInfo`, and `fullTermPolicyRatingResult`.

***

### 2026-05-15

#### New: Company Files API (folders + files)

Added 10 endpoints for managing company-level folders and files:

**Folders:** `POST`, `GET`, `GET /{folderId}`, `PATCH /{folderId}`, `DELETE /{folderId}` under `/api/v1/external/companies/{companyId}/folders`

**Files:** `POST` (multipart upload), `GET /{fileId}`, `GET /{fileId}/content` (binary download), `PATCH /{fileId}`, `DELETE /{fileId}` under `/api/v1/external/companies/{companyId}/files`

Key capabilities:

* Create nested folder hierarchies with `parentFolderId`
* Upload files via `multipart/form-data`, optionally placing them in a folder
* Stream binary file content with correct `Content-Type` and `Content-Disposition` headers
* Rename and move files/folders (including moving to root by setting parent to `null`)
* Recursive soft-delete of folders (deletes all contents)

All endpoints use `company.file:{action}` permissions. See the [Company Files overview](/api-reference/company-files/overview) for details.

<Note>
  These are company-level file endpoints only. Entity-scoped file endpoints (attached to exposures, policies, events) are planned for a future release.
</Note>

***

### 2026-05-01

#### Event→Policy relationship migrated to `eventPolicy` Join field (additive)

Event responses now surface the associated policy in two places: the existing
top-level `policyId` and the new `eventPolicy` key inside `fieldModelV1Data`.
Both reflect the same value — `policyId` stays at the top level for backwards
compatibility, while `eventPolicy` is the underlying `Join: Policy` field
where the value is stored.

**What changed:**

* `GET /api/v1/external/companies/{companyId}/events` and `/events/{eventId}`
  responses include `eventPolicy` inside `fieldModelV1Data` alongside the
  pre-existing top-level `policyId`. No fields were removed.
* `POST` and `PUT` event endpoints continue to accept `policyId` as a
  top-level request param. The server maps it to
  `fieldModelV1Data.eventPolicy` internally; clients that already submit
  `policyId` need no changes.
* `policyId` is no longer stored on a dedicated `events.policy_id` column on
  the FMV1 read/write paths — it now lives in `fieldModelV1Data.eventPolicy`,
  consistent with how `eventInsureds` was migrated previously. The external
  API contract is unchanged for existing integrators.

This is a non-breaking change. Integrators can ignore `eventPolicy` and
continue using `policyId`, or migrate to reading the value from
`fieldModelV1Data.eventPolicy` to align with the rest of the field model.

***

#### `Address.zipCode` must be a JSON string (breaking)

`Address` object-primitive writes that send `zipCode` as a JSON number are now rejected with `400` and `problemCode: "InvalidAddressZipCode"`. Previously, numeric ZIPs were accepted by the API but silently lost their leading zeros — `02140` parses as `2140`, corrupting the stored address.

**Affected endpoints:** every FMV1 create/update endpoint that can carry an `Address` value (top-level field or nested inside a custom object), including all exposure, event, quote, policy-transaction, and custom-object writes.

**What to send:** quote ZIP codes in your payload — `"zipCode": "02140"`, never `"zipCode": 02140`. The [Address sub-field reference](/api-reference/object-primitives/overview#address) and the [`Fmv1Address` OpenAPI schema](/api-reference/object-primitives/overview#address) both call this out explicitly.

**Why this is most likely to bite:**

* **Spreadsheets** (Excel / Google Sheets) auto-coerce ZIP-shaped cells to numbers — export as text or wrap in `=TEXT(...)` before serializing.
* **OpenAPI clients / codegen** that infer the `zipCode` JSON type from a sample value rather than the schema (which has always been `type: string`).
* **LLM-generated requests** that "helpfully" unquote numeric-looking strings.

***

### 2026-04-29

#### Documentation: Object Primitives

* New [Object Primitives reference page](/api-reference/object-primitives/overview) documenting the FMV1 built-in object shapes — sub-field tables, JSON examples, the completeness rule, and List cardinality.
* OpenAPI spec now exports reusable object-primitive schemas under `components.schemas` for client-codegen consumers.

<Note>
  This entry originally listed `CoverageLimit` and `QuoteBindError` as built-in object primitives and claimed the schemas "mark every sub-field as `required`". Both were inaccurate: `QuoteBindError` was later removed (2026-06-07) and `CoverageLimit` was never a built-in primitive (it is a tenant-config custom object — see the 2026-06-26 correction). The actual built-in primitives are `Address`, `Currency`, `Date`, and `StringOrNumber`, and requiredness is **per-primitive**, not blanket.
</Note>

***

### 2026-04-28

#### Strict object-primitive sub-field validation (breaking)

When a request body for an FMV1 create/update endpoint includes an object-primitive value (`Address`, `Currency`, `Date`, `StringOrNumber`), its required sub-fields must now be present and non-empty. Affected endpoints:

* `POST/PATCH /api/v1/external/companies/{companyId}/exposures` and `/exposures/{id}`
* `POST/PATCH /api/v1/external/companies/{companyId}/events` and `/events/{id}`
* `POST/PATCH /api/v1/external/companies/{companyId}/quotes` and `/quotes/{id}`
* All segmented policy transaction endpoints (`new-business`, `endorse`, `renew`)
* `POST/PATCH /api/v1/external/companies/{companyId}/custom-objects/{objectType}` and `/{objectId}`

**What changed:**

* A `null`, `undefined`, or empty-string (`""`) value on a *required* sub-field of a present object-primitive value now returns `400` with `Field '<parentField>' is missing required sub-field '<refId>'`. Previously, partial object primitives were silently accepted and surfaced as confusing rating errors downstream.
* Numeric `0` (e.g. `Currency.value: 0`) and Boolean `false` are still valid — the rule only treats `null` / `undefined` / `""` as missing.
* **Whitespace-only strings (e.g. `"   "`) are NOT treated as missing** by this validator. A consumer sending `county: "   "` will pass this check; downstream rating may still reject it. Trim/normalize sub-field strings client-side before submitting.
* Custom-object sub-fields keep their existing `requiredCondition` rules unchanged.
* Omitting the parent object-primitive field entirely is unchanged — the strict rule only fires when the parent value is provided.
