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.
Before — a field typed
Object: Address:
Object: AddressV2:
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
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→ rootpolicyNumberfullTermPolicyInfo.policyStartDate→ rootpolicyStartDatefullTermPolicyInfo.policyEndDate→ rootpolicyEndDatefullTermPolicyInfo.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 ownpreviousPolicylink, which you already send onPOST /transaction/renewand which the platform writes as thePolicy<N:1:previousPolicy>Policyrelationship. Read the link back from the policy’spreviousPolicyfield 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→ rootprimaryInsuredName/primaryInsuredId. These left the container on 2026-08-10; see that entry.
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’ssummary.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 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→ rootpolicyNumberfullTermPolicyInfo.policyStartDate→ rootpolicyStartDatefullTermPolicyInfo.policyEndDate→ rootpolicyEndDatefullTermPolicyInfo.primaryInsuredName/.primaryInsuredJoin→ rootprimaryInsuredName/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.
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:
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/primaryInsuredIdfields 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.
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 optionalinvoicePlan, 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:
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 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 oflineItems). Each component is{label, group, kind, value[, earningBasis]}:kindis the new closed classifier (Premium,Taxes,Fees,BrokerCommission,ProgramCommission,Other);groupis now only an invoice-grouping name;valueis a plain number (the{value, code}Currency wrapper is gone — everything is USD);earningBasis(pro-rataorfully-earned-at-inception) is optional — omitted means pro-rata, and a basis is rejected on kindOther. - 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. policyGrandTotalis 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 ownpolicyPremium/policyTaxes/policyFees/policyGrandTotal.)- The
Billing Tabledisplay format is renamedPricing Tablein/configurationfield locations.
Removed: the old key is no longer accepted in requests
ThefullTermPolicyBillingInfo 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
fullTermPricingInfoeverywhere the old key used to appear (version/transaction responses, the policy list summary, saved views, exports). Read totals from the five rollups; itemized charges frompricingComponents. Bordereau column ids are unchanged (policyPremium,policyPremiumChange) — only configured field-column paths move (e.g.fullTermPricingInfo.taxes). - Writing. Send
pricingComponentsunderfullTermPricingInfo(one component per charge, classified bykind) and let the platform compute the rollups. A payload namingfullTermPolicyBillingInfonow fails with a400(unknown key on the transaction channels; undeclared field insidefieldModelV1Data.policy).
2026-08-05 (policy field data is no longer nested)
Breaking: policy transactions send and return the field data unwrapped
ThefieldModelV1Data.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:
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
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-reviewstarts one and answers202with therunIdto poll. Nothing is reviewed inside the request — the review is background work. There is no request body, and noactionIdorIf-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. Requirescompany.payment:update.GET …/bill-reviewreturns the latest run —status, thepassedverdict,error, the pinnedinvoiceHeadJournalId, and thefindings. Requirescompany.payment:read.
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.
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 — 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 backpaidwithbalanceDueCents: 0, and the gesture’s net cash equals thebalanceDueCentsyou asked to pay. Only the literaltrueis accepted;payBalanceDue: falseis a400.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}andPOST /financials/invoice-drafts/{invoiceId}/finalize→ a draft is an ordinary invoice withstage: "draft"; useGET /financials/invoices/{invoiceId}andPOST /financials/invoices/{invoiceId}/finalize.POST /financials/event-transactions/import→ usePOST /financials/events/{eventId}/import.GET /financials/invoice-types→ useGET /financials/config/categories.POST /financials/payees/reassign→ usePOST /financials/payees/{payeeId}/merge.
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 singleInvalidEntityShape 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 400s naming the offending
item. See Entities → 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.
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.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 paymentIds) 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’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 for the design rules: client-suppliedactionId 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 mintspaymentIdand computes allocations pro-rata.DELETE …/invoices/{invoiceId}/payments/{paymentId}— remove a payment; the horizon rule’s companion reserve unwind composes automatically.POST …/invoices/{invoiceId}/voidandPOST …/invoices/{invoiceId}/restore— write the charges down to zero and back.
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→ usePOST /financials/events/{eventId}/import.GET /financials/invoice-types→ useGET /financials/config/categories.POST /financials/payees/reassign→ usePOST /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=truefor invoices with no event and no policy link); cursor-paginated. Deleted invoices are hidden. Every row carriesheadJournalId.GET /api/v1/companies/{companyId}/financials/invoices/{invoiceId}— the whole current invoice document, its live payments, andheadJournalId— 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 withnormalBalanceso clients orient displays.GET /api/v1/companies/{companyId}/financials/entities/{entityType}/{entityId}/balances— one entity’s balance slice (entityTypeisevent|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; optionalcategoryIdfilter.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=trueto resolve old references.
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-businessandPOST /api/v1/companies/{companyId}/policies/{policyId}/transaction/endorseaccept an optionaldisplayAuthorstring (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. Requirespolicy:update.- Transaction read responses (get/list) now include
displayAuthor(nullunless 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:
newBusinessandrenewalmint a brand-new policy (a renewal’s new policy is linked back to the expiring term), whileendorsement,cancellation, andreinstatementtransact against their existing source policy in place. The responsepolicyIdis the policy the transaction landed on — the new policy fornewBusiness/renewal, the existing source policy for the in-place types. - Response:
201 { policyId }. - Already bound: a quote that is already
boundis rejected with409, and the error body carriesreferencingPolicy(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-businesstransaction enforces — notably the primary-insured identity onfullTermPolicyInfo(primaryInsuredJoin, referencing an existing Exposure, plusprimaryInsuredName). A quote that fails them returns a structured400. - 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}/notesaccepts an optionaldisplayDate; note responses expose it (nullwhen unset).GET …/notesnow orders by the display date (displayDatewhen set, elsecreatedAt), so backdated notes interleave correctly.POST /api/v1/companies/{companyId}/files(upload intent) accepts an optionaldisplayDate; file list items and file metadata expose it.GET …/filesnow 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 withstarterSheet:modulesis authoritative for every axis and feature (the server unions it with the always-on basecore+defaultbut appends no default feature modules), whereasstarterSheetexpands to a complete out-of-box config. Supplying both is a400(InvalidModuleSelection); an unknown module id is a400(UnknownStarterModule); selecting two modules from the same pick-one axis — e.g. two raters or two exposure schemes — is a400(InvalidModuleSelection).replace— an overwrite guard. Seeding a company that already has a non-empty configuration is now refused with a409(ConfigAlreadyExists) unlessreplace: true; a never-configured company still seeds without it.GET /api/v1/companies/{companyId}/configuration/seed/optionsnow returns amodulesarray — the full starter-module catalog, each entry carryingid,label,description,defaultSelected, its axisgroup(always-on/exposure-scheme/rating/policy-number/feature), andalwaysOn— alongside the existingstarterSheetoptions.
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 acolumnsentry{"kind":"field","path":"…","header":"…"}(on the rendered surfaces, list the fixed columns you want alongside them —columnsis 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 initialcolumnsdesign note that the list endpoint took nocolumnsat 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.
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
columnspreserves the default output — the 12 fixed columns in canonical order. - The JSON list endpoint’s
columnssupport (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 theforms: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 atotalCount) and optionally filtered bycategoryand/orkind. Each item includesnumber,name,kind(templatevsstatic),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 itsFM-XXXXnumber. Returns{ downloadUrl, fileName, contentType, expiresAt }, wheredownloadUrlis a 15-minute signed URL the consumer GETs the bytes from directly.
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:
targetis now populated with the{ kind, id }file or folder the run parsed (previously alwaysnull). Runs that predate grouping still reporttarget: nulland are taggedlegacy: true.- New optional fields per record:
stages(per-stagestatus— including the distinctretryingandpending— plusattempts,error,finishedAt),createdEntities(entityType/entityId/deletedfor every entity the run created),extractionReady,rerunOfRunId,reusedExtractOfRunId, andlegacy. statusis unchanged: still exactlyrunning/succeeded/failed, with every internal retry state reading asfailed(per-stageretryingdetail now lives instages).
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/staticuploads a.pdfstatic form (a certificate, handout, or any finished PDF) into a company instance under a chosencategory(event,quote-flow, orquote-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 — samenumber,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 returns400form-template-type-mismatch/form-static-type-mismatch), andcategoryis not part of the request (a replace never re-scopes). An unknown number returns404. 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 stableFM-XXXXnumber(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 returns404(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-runslists a company’s parse runs, newest-first and paginated (1-basedpage/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 arunId,flow(nullfor the flow-agnosticextractstage),status(running/succeeded/failed— the internalfailed_*retry states collapse tofailed),attempts, optionalerror, and timestamps.targetis currently alwaysnull. 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,
operationIds, 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-templatesuploads a.docxform template into a company instance under a chosencategory(event,quote-flow, orquote-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 a400whosedetailsname 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 operationIds 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(operationIdimportFmv1ConfigurationJson→importFmv1Configuration)POST /configuration/export-json→POST /configuration/export(operationIdexportFmv1ConfigurationJson→exportFmv1Configuration)POST /configuration/compare-json→POST /configuration/compare(operationIdcompareFmv1ConfigurationJson→compareFmv1Configuration)POST /configuration/validate-json→POST /configuration/validate(operationIdvalidateFmv1ConfigurationJson→validateFmv1Configuration)
operationIds 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. UsePOST /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. UsePOST /configuration/export-json(returns current config as a structured JSON body) instead; its output is the exact shapeimport-jsonaccepts, soexport-json→ edit →import-jsonis a lossless round-trip. - Unaffected: the in-app onboarding spreadsheet import/export UI. The
company.configuration:import/:exportpermissions 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/optionsreturns the validstarterSheetvariants, each with a description and a default marker, plus thedefaultStarterSheetapplied whenstarterSheetis 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/generatereturns the exact JSON configuration a seed would apply for the selectedstarterSheet— 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 }; nogoogleOAuthToken. 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/seedis unchanged — it still applies starter content directly to the company. ItsstarterSheetfield now also documents anenumof 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.CoverageLimitwas never a built-in object primitive. The docs (and theFmv1CoverageLimitOpenAPI schema) described aCoverageLimitprimitive with required{ coverageLimitName, coverageLimitAmount }. No such primitive exists at runtime. The thing tenants actually model is a custom object namedNumberLimit({ numberLimitName, numberLimitAmount }), defined in the (default) tenant configuration —Coverage.coverageLimitsis a list of it. The schema was renamedFmv1CoverageLimit→Fmv1NumberLimitwith 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, andStringOrNumber. Earlier entries listedCoverageLimit(never real) andQuoteBindError(since removed), and omittedStringOrNumber. The Object Primitives reference 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), andStringOrNumber(kind).Addresssub-fields are all optional — a partial address is valid and is not rejected.timezoneon aDateis optional in the DMY shape. - Date input shapes documented. A generic
Datefield 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-businessexample mixes framework fields (primaryInsuredJoin,primaryInsuredName,fullTermPolicyInfodates) 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 endpointPOST /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 bodylessPOSTseeds the product default. - Response:
{ success: true, message? }on success; a400with 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 bothPUT 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}
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:deletefor the Exposure update/delete,policy:updatefor the policy cancel/endorse/reinstate transactions, andpolicy:deletefor transaction delete. Create/read operations keep theircompany.-prefixed permissions. The Event configuration endpoint requirescompany.event:export. - Raw
Authorizationheader. External API authentication takes your API key as the rawAuthorizationheader value with no scheme prefix — notBearerand notApiKey, and neverX-API-Key. See Generating API Keys. - 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 (
fieldModelV1Datawith epoch-secondcreatedAt/updatedAt, no top-levelcompanyId) and its{ items, hasMore, totalCount }/ zero-basedpageNumberlist 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’sfolderId(a folder of the placement’s owner, ornullfor the owner’s top level) and/orcategory(free-text ≤255 chars, ornullto clear). Absent fields are untouched; at least one must be present. Returns200 { placementId, fileId, entityType, entityId, folderId, category }; the file’s other placements are never affected. Placement ids come fromGET /files/{fileId}/placementsor the share response. Requirescompany.file:update.- The
409 Conflictbody returned byPATCH /files/{fileId}forfolderId/categoryon 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 andcategory 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 }]). Requirescompany.file:read.POST /api/v1/external/companies/{companyId}/files/{fileId}/placements— share the file to another owner (entityType,entityId?, optionalfolderIdof the target owner). Returns201 { placementId, fileId, entityType, entityId, folderId }; a duplicate share to the same owner is409 Conflict. Requirescompany.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). Requirescompany.file:delete.- The file responses now carry placement information: list items gained
placementIdandplacementCount;GET /files/{fileId}gainedplacementCount, 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 changedisplayName; an owner-lessfolderId/categoryupdate is ambiguous across placements and returns409 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-textcategory 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 optionalcategoryfield alongsidedisplayName/folderId: a string sets the label, an explicitnullclears it, and an absent field leaves it untouched.- The file responses (
GET /files/{fileId}metadata and theGET /fileslist items) already includecategory(nullwhen 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 for the new workflow.- Upload is now a two-phase handshake.
POST /api/v1/external/companies/{companyId}/filesno longer acceptsmultipart/form-data; it takes a JSON upload intent (entityType,entityId?,folderId?,fileName,contentType,byteSize) and returns201 { fileId, versionId, uploadUrl }. PUT the bytes touploadUrl(pinned to the declared content type and byte size, 15-minute expiry), thenPOST /files/{fileId}/finalizewith theversionIdto 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 /foldersreturns the owner’s whole tree as{ folders: [{ id, parentFolderId, name }] }(no pagination), andGET /folders/{folderId}returns{ folder, folders, files, totalCount }instead of the mixedcontentType-discriminated item list. GET /files/{fileId}returns the new metadata shape —displayName/fileName/contentType/byteSize/status/folderId/category/createdAt/updatedAt/uploadedAt/uploadedByreplace the legacyname/mimeType/entityName/userDatefields.PATCH /files/{fileId}renames withdisplayName(wasname) and/or moves withfolderId; it returns{ id }(was the full file).PATCH /folders/{folderId}keepsname/parentFolderIdbut 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 newGET /files/{fileId}/download-url, which returns{ url, expiresAt, fileName, contentType, byteSize }(requires the newcompany.file:downloadpermission), and fetch the bytes from the signedurl.- The
multipart/form-dataupload 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?, optionalfolderIdplacement filter, 1-basedpage/pageSize). Previously files were only discoverable through folder contents. Requirescompany.file:read.POST /api/v1/external/companies/{companyId}/files/{fileId}/finalizeandGET /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. (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-basedpage/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 notePATCH /api/v1/external/companies/{companyId}/notes/{noteId}— Update a note’s bodyDELETE /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 bystatus,assigneeId,entityType,entityId). Requirescompany.task:read.POST /tasks— Create a task. Returns{ id }. Requirescompany.task:create.GET /tasks/{taskId}— Get a single task. Requirescompany.task:read.PATCH /tasks/{taskId}— Partial update (only changed fields; unknown fields rejected). Requirescompany.task:update.DELETE /tasks/{taskId}— Soft delete. Returns{ id, deleted: true }. Requirescompany.task:delete.
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.
Breaking: Full-term policy transaction reshape
The segmented Policy Transaction API moved to the full-term (“Model-B”) design. Affectsnew-business, endorse, cancel, reinstate, and renew.
- Term bounds come solely from
fullTermPolicyInfo.policyStartDate/policyEndDateare read fromfieldModelV1Data.policy.fullTermPolicyInfoon NEW_BUSINESS / RENEW — the top-levelpolicyStartDate/policyEndDate(and RENEW’s top-levelpreviousPolicyId/newPolicyStartDate/newPolicyEndDate) parameters are removed.previousPolicyIdnow lives infullTermPolicyInfo. fullTermPolicyBillingrenamed tofullTermPolicyBillingInfoin every request, response, and bordereau column path.policyStatusis a segment-scoped policy field with lowercase values"active"/"cancelled"— no longer insidefullTermPolicyInfo.- ENDORSE now has five channels:
deltasXORfullTermDeltas(the latter restricted topolicy.fullTermPolicyInfo, no dates), plus additivefullTermPolicyBillingInfo,fullTermPolicyRatingResult, andcrossSegmentRatingOutputs. List elements are addressed by predicate —exposures[id = '…']. - CANCEL / REINSTATE take the date plus optional whole-object
fullTermPolicyBillingInfo/fullTermPolicyRatingResult. CANCEL flips per-segmentpolicyStatusto"cancelled"and records a singlecancellationEffectiveOnDate(uniform across the term); REINSTATE flipspolicyStatusback to"active"and clearscancellationEffectiveOnDate(no reinstatement date field). A reinstate that would leave a coverage gap is rejected — that scenario is a new policy term.policyEarlyTerminationDateis removed. - Rating output split into
fullTermPolicyRatingResult(policy-root, hoisted) andcrossSegmentRatingOutputs(element-level, inline). Responses hoistfullTermPolicyInfo,fullTermPolicyBillingInfo, andfullTermPolicyRatingResult.
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-TypeandContent-Dispositionheaders - Rename and move files/folders (including moving to root by setting parent to
null) - Recursive soft-delete of folders (deletes all contents)
company.file:{action} permissions. See the Company Files overview for details.
These are company-level file endpoints only. Entity-scoped file endpoints (attached to exposures, policies, events) are planned for a future release.
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}/eventsand/events/{eventId}responses includeeventPolicyinsidefieldModelV1Dataalongside the pre-existing top-levelpolicyId. No fields were removed.POSTandPUTevent endpoints continue to acceptpolicyIdas a top-level request param. The server maps it tofieldModelV1Data.eventPolicyinternally; clients that already submitpolicyIdneed no changes.policyIdis no longer stored on a dedicatedevents.policy_idcolumn on the FMV1 read/write paths — it now lives infieldModelV1Data.eventPolicy, consistent with howeventInsuredswas migrated previously. The external API contract is unchanged for existing integrators.
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 and the Fmv1Address OpenAPI schema 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
zipCodeJSON type from a sample value rather than the schema (which has always beentype: string). - LLM-generated requests that “helpfully” unquote numeric-looking strings.
2026-04-29
Documentation: Object Primitives
- New Object Primitives reference page 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.schemasfor client-codegen consumers.
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.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}/exposuresand/exposures/{id}POST/PATCH /api/v1/external/companies/{companyId}/eventsand/events/{id}POST/PATCH /api/v1/external/companies/{companyId}/quotesand/quotes/{id}- All segmented policy transaction endpoints (
new-business,endorse,renew) POST/PATCH /api/v1/external/companies/{companyId}/custom-objects/{objectType}and/{objectId}
- A
null,undefined, or empty-string ("") value on a required sub-field of a present object-primitive value now returns400withField '<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 Booleanfalseare still valid — the rule only treatsnull/undefined/""as missing. - Whitespace-only strings (e.g.
" ") are NOT treated as missing by this validator. A consumer sendingcounty: " "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
requiredConditionrules unchanged. - Omitting the parent object-primitive field entirely is unchanged — the strict rule only fires when the parent value is provided.
