V1 API Changelog
Track changes, additions, and deprecations to the V1 API.2026-09-29 (Structured configuration field type inputs)
Additive. Full configuration inputs and theadd-field patch change accept
typeInfo alone, containing a kind and structured kind-specific args.
Legacy type and JSON-string typeArgs remain accepted. If both representations
are present, they must agree semantically; conflicting declarations are rejected
with the field identity. Canonical configuration exports now contain only
typeInfo for each field type, without type or typeArgs duplicates.
See Field type declarations.
2026-09-28 (Bordereau export presets on behalf of a member)
Additive.POST /api/v1/companies/{companyId}/export-presets accepts
entityType: "bordereau", creating a private preset the owner finds in their
Export Bordereau screen. Field keys are validated against the bordereau export
surface: fixed:<key> for a built-in bordereau column (for example
fixed:policyNumber) and field:<key> for a column configured on the
bordereau surface. Any other key is refused with UnknownExportFieldKey. See
Export presets.
2026-09-28 (Policy validation and repair)
Policy validation includesversionRegressions, comparing consecutive stored
versions at matching segment dates and reporting paths and counts without field
values. Company-scoped POST /api/v1/companies/{companyId}/policies/repair
supports segmentHashes, joinHistory, and compare-and-swap fieldValues repairs.
Repair requires a staff Bearer token or verified platform service identity and
rejects API keys. Preview repairs with a dry run before applying.
The unused policies/{policyId}/field-value-rewrites endpoint is removed;
use the fieldValues repair kind.
2026-09-26 (Repeatable calculated fields and complete bind checks)
Response shape change. Entity and policy configuration schemas mark each calculated field withx-repeatable (boolean) instead of x-purity
(pure/impure): true means the same stored data always gives the same
answer, which every LIVE field satisfies. The slimmed schema from the MCP
get_entity_schema tool carries repeatable instead of purity. Check Bind
Conditions (GET /api/v1/companies/{companyId}/quotes/{quoteId}/bind-conditions)
no longer returns skippedImpureConditions: every bind rule is now checked, and
a rule the check cannot evaluate ahead of the bind appears in failures with a
message naming it, so a green check is never partial. Existing integrations with
retained compatibility keep their current response.
2026-09-25 (Event name)
eventName on an Event is now read-only: the platform recomputes it on every
save from the linked exposures and claimant, so it follows them as they change.
To give an event a name of your own, write the new userSetEventName field —
it takes precedence over the derived name for as long as it is set, and clearing
it hands the name back to the derivation. A value sent for eventName is
ignored (the write still succeeds); integrations that renamed events by writing
eventName should write userSetEventName instead.
2026-09-25 (Export presets on behalf of a member)
Additive.POST /api/v1/companies/{companyId}/export-presets creates a
private saved export preset owned by the company member named in ownerUserId,
who then finds it in their export screen. Field keys are validated on write
against the company’s export surface: unknown keys are refused with
UnknownExportFieldKey, and a name the owner already uses is refused with
ExportPresetNameTaken. Requires export-preset.create-for-member. See
Export presets.
2026-09-25 (Policy contacts)
Additive. Policy contacts can now be managed over the external API.GET /api/v1/companies/{companyId}/policies/{policyId}/contacts lists the policy’s
current Directory contacts with display names, POST .../contacts links an
existing Person (idempotent on repeat), and DELETE .../contacts/{personId}
removes only the association. Contacts are unversioned: these calls create no
policy version, and a contacts value sent in transaction data is still
ignored. Reads require policy.view and person.view; writes additionally
require policy.endorse. See Contacts.
2026-09-25 (Unified configuration exports)
Response shape change. Canonical configuration exports define each custom object once incustomObjects, combining its key, labels, optional display-name expression,
and subFields. Option sets appear only in optionSets; objectTypes, objects,
and optionSetTypes are no longer emitted. Empty definitions remain valid.
Imports continue accepting either complete format. Re-fetch export and metadata
before importing with expectedContentHash, since the exported bytes change.
Existing integrations with retained compatibility keep their current export
representation. Metadata and expectedContentHash refer to the representation
returned to that caller; use the same credentials for all three operations.
2026-09-24 (Unified configuration input)
Additive. Configuration import, validate, compare, and data-validation runs also acceptcustomObjects rows containing key, name, pluralizedName,
optional displayNameExpression, and subFields. In this format, optionSets
is the only option-set declaration. Omit objectTypes, objects, and
optionSetTypes; mixing the two formats is rejected. Empty objects and option
sets remain supported. Existing split-format inputs and exports are unchanged.
2026-09-23 (Page layouts use the owning entity’s view permission)
Additive.GET /api/v1/companies/{companyId}/pages/{pageKey}/layout returns
one page’s cards and field placements for external renderers. It requires the
page entity’s view permission, such as event.view or quote.view, without
configuration-authoring access. The projection includes reachable nested cards,
rendering arguments, and unevaluated display/required conditions; record reads
and actions retain their own authorization. Authoring describe/* permissions
are unchanged. See Page layouts.
2026-09-22 (Replace Event Open/Close History honours the field’s write group)
Tightening. Replace Event Open/Close History replaces the wholeeventOpenCloseHistory field, so when a company’s
configuration gives that field a writePermission, the call now also requires
the matching field write group (event.field-<value>) on top of
event.edit, and returns 403 field-write-permission-required without it.
This is the same rule a generic event update naming the field already follows.
Companies whose configuration does not restrict the field see no change, and
Close Event and Re-open Event are unaffected.
2026-09-22 (editing or deleting a paid invoice requires invoice.edit-paid)
Tightening. Update Invoice and
Delete Invoice now also require
the invoice.edit-paid permission when the invoice has a recorded payment
(status paid or partially_paid). A key holding only invoice.edit /
invoice.delete receives 403 on those invoices; owed invoices are
unaffected. Recording and removing payments keep their own permissions
(payment.record, payment.delete).
The permission has been in the catalog since the roles overhaul and is held by
the Admin and Finance standard roles, so keys on those roles see no change.
Keys on a custom role that edits or deletes paid invoices need the permission
granted in the role editor.
2026-09-21 (rating workflows can declare warnings; the configuration schema documents their rate gates)
Additive. A rating workflow can now declare warnings, a list of
{expression, message} advisories alongside its existing preconditions:
- Each expression that does NOT evaluate true when the workflow rates returns a
severity: warningdiagnostic with the coderating-workflow-warningand the configured message, beside the rateddata. The rate still succeeds — a warning gates nothing, which is what separates it from a precondition. - Like other warnings raised outside a single stage, these carry no
location. As always, the failure signal is the absentdatafield, never the mere presence ofdiagnostics. - The polarity matches
preconditions: true means satisfied and silent.
preconditions and billingPreconditions, which it carried but never
described.
No action required: a workflow that declares no warnings is unchanged, and
callers that ignore diagnostics on a successful rate are unaffected. Clients
that branch on diagnostic codes should treat unknown codes as advisory.
2026-09-19 (Quote write consistency and rating-to-bind guidance)
External API responses now carryCache-Control: no-store, so an immediate
GET after a successful quote PATCH cannot be served from a stale client or
intermediary cache. Bound quotes reject generic updates consistently, including
updates initiated outside the external API; use policy transactions for later
changes.
The rating guide now documents the complete rate → persist → verify → bind
sequence. When rating returns fullTermPricingInfo, copy that container into
the quote PATCH before binding. Bind reads the persisted quote, not the earlier
rate response. The policy-by-id reference also documents the versioned read
path and its required detail parameter.
2026-09-18 (Field write groups (writePermission))
A field configuration row may now carry writePermission: "<kebab-case>".
Every field of one entity sharing a value forms a field write group, the
grantable permission <entity>.field-<value> (e.g. event.field-reserves),
shown in the role editor beside catalog permissions. A create or update body
naming a locked field is refused WHOLE with 403 and error code
field-write-permission-required unless the caller’s role holds the group.
Unlike readOnly / systemManaged, the value is never silently stripped.
The per-entity GET .../configuration endpoint now emits x-write-permission
on a locked field’s JSON-Schema property. writePermission round-trips
through import/export, describe/fields, and the add-field / update-field
config patch verbs (null clears it on update-field). Conversion Rules and
platform writes are unaffected.
2026-09-18 (Saved-quote rating records its workflow selection)
A successfulPOST /api/v1/companies/{companyId}/quotes/{quoteId}/rate now
records the requested ratingWorkflowName as Quote system metadata. This lets
later saved-quote workflows identify the exact workflow used by an external
create → rate → persist → bind flow, including workflows with a billing tail
and no freshness declaration. A failed rate leaves the previous selection
unchanged.
The returned rating fields remain caller-persisted: the endpoint does not change
the quote field bag or updatedAt, and it does not record a rating run. Continue
to persist the fields you need with PATCH /entities/quote/{entityId} before
binding.
2026-09-17 (Delete all Policy data)
AddedPOST /api/v1/companies/{companyId}/policies/deleteAll. It hard-deletes
the complete Policy entity-data slice — anchors, live and archived transactions,
deltas, segments, relationships, and Policy-scoped sequence counters — so a
breaking configuration import can be retried without database access. It is
Sandbox-only, requires company.reset, is safe to retry, and returns the total
rows removed. Live Quote/Event references and financial claims return 409
without deleting anything. Legacy SUPER_ADMIN keys continue to authorize
through the existing permission mapping.
2026-09-16 (Consistent validation findings and pagination)
Configuration validation now returns{ isValid, issues }. Each issue has
message, fieldPath as ordered string segments, and severity: "error" | "warning".
Optional code, reason, and expected preserve structured validator details.
Warnings do not make a configuration invalid; an empty path means the issue has
no known field location.
Synchronous data validation and saved run findings use the same record shape:
{ entityType, entityId, adheres, extraKeys, issues }. Saved findings now preserve
the structured details previously available only from synchronous validation.
Both paginated responses include { items, totalCount, page, pageSize }.
Batch validation accepts one-based page and pageSize instead of offset;
its default and maximum page size are 100. Saved findings default to 50 and
cap at 500. The response reports the effective size after clamping. Continue
while page * pageSize < totalCount. Batch responses retain their timestamp,
entity type, and summary. A completed run can still contain findings; wait for
completion before treating its total as stable.
Existing integrations observed using the earlier response contracts retain
compatibility while migrating. New integrations should use the contract above.
2026-09-16 (Sandbox Reset and company lifecycle)
AddedGET /api/v1/companies/{companyId}/lifecycle (company.view) and
POST /api/v1/companies/{companyId}/reset (company.reset). Reset clears Sandbox
application data while preserving configuration and access, restarts sequence
counters, and commits its minimal audit receipt in the same transaction. Live,
Retired and uninitialized companies are protected for all callers. See
Companies.
2026-09-16 (Policy pricing presence)
Policies and quotes supportfullTermPricingInfo: null for no policy pricing.
A present pricing object requires a pricingComponents array and the four
server-computed numeric rollups. Empty components return zero rollups.
Legacy API input with omitted or null inner components continues to normalize to
an empty array before stamping. Pricing omission on lifecycle requests retains
existing inherit/derive behavior; pricing presence is not a bind requirement.
See the Pricing Contract.
2026-09-16 (Submission and quote lifecycle actions)
Three existing lifecycle operations are now available through the V1 API:inProgress or
complete quote to cancelled. The endpoints require submission.close,
submission.reopen, and quote.cancel, respectively.
The same strict transition guards used by the app apply: invalid transitions
return 409 with the existing machine-readable codes. Generic PATCH remains
unchanged and still refuses guarded submissionStatus and quoteStatus
transitions with GuardedStatusFieldWrite.
2026-09-16 (Embedded exposure create validation)
An embedded exposure created inline now reports every missing caller-required field for that item in oneInvalidEntityShape 400. Each userMessages entry
includes the embedded item path, such as exposures[1]; the enclosing write
still rolls back as one transaction.
The create-mode branch in the entity configuration schema now publishes that
same caller-required contract. Calculated values, server-seeded defaults, and
joins are excluded from its required array, matching standalone Exposure
create behavior.
2026-09-15 (Privileged notes)
Every note now carries aprivileged flag, and both Create Note and Update Note
accept it. A privileged note is visible only to callers holding the new
note.view-privileged permission; for everyone else it is absent — omitted from
lists and their totalCount, and 404 by ID. Sending privileged (true or
false) requires that permission, and is refused with 403 without it. Update
Note is now a true PATCH: send note, privileged, or both.
2026-09-15 (Quote rating advisories)
Persisted quote Bind and Complete Quote allow stale or unverifiable rating without an override. Check Bind Conditions returns non-blockingadvisories separately from failures and identifies endorsement billing checks with checkScope: endorsement-billing. With policy invoicing enabled, current real billing and matching invoices remain required; matching posted invoices need no new plan. Ordinary policy conditions and financial integrity continue to apply.
2026-09-15 (Permission names)
Every endpoint’s Required permission now names aresource.verb permission
from the new catalog (for example policy.view, invoice.create,
configuration.edit) instead of the old company.<entity>:<action> strings.
Your key’s access is unchanged: every role carries both names while the old
ones are retired, so no request that succeeded before fails now. The
entity-types endpoint advertises the new names, and it, like the other reads
every integration needs, now requires only a valid API key for the Company.
Wiping a Sandbox Company’s data (entities/{entityType}/deleteAll,
policies/deleteAll, financials/deleteAll) and forcing a configuration import past the
breaking-change gate (?force=true) are the Admin permission company.reset.
A Live Company refuses both, whoever asks: demote it to Sandbox first.
2026-09-14 (Rating stage property validation)
Configuration validation now rejects every unknown property on a rating workflow stage. Stage properties must bestageType, raterSpec, callOncePerPath,
outputPath, or inputs. Correct misspelled or unsupported keys before importing;
they are no longer silently discarded. Existing supported stage declarations
keep the same behavior.
2026-09-11 (Financials document v2)
Financials configuration export and import now usedocumentVersion: 2.
Policy invoice types no longer nest line items. policyFinancials.lineItemTypes
contains each item’s name, immutable direction, lifecycle, and
allowedOn: [{ invoiceType, label? }] associations. Labels may differ by invoice
type; null explicitly uses the canonical name and omission leaves a label
unconfigured. Additive import preserves configured labels and defaults, including
explicit clears. It refuses ambiguous legacy identities and never merges them.
Version 1 imports receive an explicit unsupported-version error.
2026-09-11 (Field placement names)
OptionSet displays useOption Labels. Join placements use Join Picker,
Join Link, Join Card List, or Join Table; Pointer placements use the
corresponding Pointer names. EmbeddedExposure displays use Embedded Exposure Link, Embedded Exposure Card List, or Embedded Exposure Table Display.
Configuration imports enforce each field type’s supported placement names.
Stored field values and relationship targets are unchanged.
2026-09-11 (Field types own structured values)
Configuration exports and the configuration schema no longer include theobjectPrimitives catalog. Address, AddressV2, Date and StringOrNumber retain
their existing field types and value shapes. Their definitions come from the
framework; tenant-authored custom objects remain in objects.
Existing configuration JSON can still be imported: the unused catalog is ignored.
Historical version downloads preserve the original saved JSON.
See Structured Field Values for payloads.
2026-09-11 (Payment scheduling defaults)
CompanypaymentDefaults now accepts paymentPeriod (upfront, quarterly, or
monthly), firstDueDate with an anchor of today or effective-date and a
daysAfter offset, and avoidWeekendSends. These choices initialize new plans;
saved preferences retain their dates and schedule. Weekend handling moves send
dates back to Friday without changing due dates.
Existing settings remain valid: an omitted period is inferred from payment count,
an omitted first due date uses the transaction effective date, and weekend handling
defaults to off.
2026-09-11 (Payee formulas and dropdown entities are independent)
A policy invoice type’spayeeDefaults.expression now accepts null to disable
automatic payee selection while retaining allowedEntityTypes. The allowed kinds
remain editable and apply to manual selections. Setting the entire payeeDefaults
object to null still clears both settings and allows all three kinds.
2026-09-10 (Financials configuration: payment and payee defaults)
Financials configuration import and export now document optional company-widepolicyFinancials.paymentDefaults and per-policy-invoice-type payeeDefaults.
Payment defaults seed installment count, owed lead days, and endorsement handling.
Payee defaults contain a pure policy formula and allowed party kinds; null records
an explicitly cleared default, while omission leaves the setting unconfigured.
Import provisions absent defaults and preserves existing edited, explicitly cleared,
and explicit legacy defaults. Import responses may include
changes.paymentDefaultsCreated and changes.payeeDefaultsCreated to describe
defaults provisioned on apply or proposed for provisioning on a dry run. Dry runs
persist no Financials configuration changes. The generated TypeScript client
includes these fields.
2026-09-10 (OFAC screening: a stored result survives a request-only write)
Behaviour change onPATCH /api/v1/companies/{companyId}/entities/Exposure/{id} and on any write carrying an embedded exposure, for a company using OFAC screening. A screening’s response half — status, screeningOutcome, screenedAt, screenedBy, screenedRequest, matches and the rest of the platform-owned sub-fields — is now arbitrated by screenedAt instead of being replaced wholesale.
- a write whose screening carries an older
screenedAtthan the stored one, or none at all, keeps the stored response half and takes the incoming request values. Previously it overwrote the result, so a client sending back request values it had read earlier could erase a completed screening. - the practical effect: a request-only write no longer blanks a stored result. It leaves that result in place, reading as stale against the values it actually ran under, which is the more accurate answer.
- a write carrying an equal or newer
screenedAtis accepted unchanged, so a fresh screening submitted through the same save still lands. - reviewing potential matches is unaffected: a review decision rides the same save and still applies.
2026-09-10 (OFAC screening: the subject name is filled in server-side)
Additive, and only for a company that has OFAC screening placed. When an Exposure is written, the platform now copies the exposure’s own name into the screening’sname if that name is blank, so the record carries the name it
would be screened under even when no screening has run.
- an Exposure that previously came back with
ofacScreening: nullmay now carry{ "name": "…" }. The field’s type is unchanged; a client reading it as “has this been screened?” should teststatus, which stays absent until a screening actually runs. - a name already present is never overwritten, including when the exposure itself is renamed. A rename leaves the stored result reading as stale against the name it actually ran under, which is the accurate answer.
- nothing here runs a screening or calls the provider, so no call is billed.
2026-09-08 (Date values: the retired { day, month, year } spelling is refused everywhere)
Breaking change on every write that carries a Date field value: entity
create and update, POST …/policies/transaction/new-business and …/renew,
POST …/quotes/rate, POST …/quotes/bind (by value), and the event lifecycle
endpoints (…/events/{eventId}/close, …/reopen,
…/open-close-history). A Date value — at any depth, including inside a
custom object or an embedded exposure — must be the canonical
{ "date": "YYYY-MM-DD", "timezone": "<IANA zone>" } object described by
Fmv1Date. timezone may be omitted and defaults to America/New_York.
- the retired
{ "day", "month", "year", "timezone" }spelling is refused with400/InvalidFieldModelV1Data, each refused value named indetails; - the event lifecycle
effectiveOnDate(and every entry of a replaced open/close history) is now anFmv1Datetoo, not the bare{ year, month, day }triple it was documented as.
value
names a civil day as { year, month, day } by design and is not a stored Date.
This retracts the 2026-09-04 note that “every other write path … is unchanged”:
those paths accepted the retired spelling as a transitional courtesy, and that
courtesy has now ended.
2026-09-07 (folders carry a category)
Additive. A folder can now carry its own category, drawn from the same per-entity-type configured list a file’s category comes from.GET /api/v1/companies/{companyId}/foldersandGET /api/v1/companies/{companyId}/folders/{folderId}returncategoryon every folder node (nullwhen uncategorized).POST /api/v1/companies/{companyId}/foldersaccepts an optionalcategory.PATCH /api/v1/companies/{companyId}/folders/{folderId}acceptscategoryas a third updatable field — a label to set, ornullto clear it. Omit it to leave the folder’s category alone.
400, exactly as on a file write,
unless the folder already carries it. A folder’s category describes the folder
only: the files inside keep their own categories, and moving a file between
folders never changes its label.
Renaming a configured category in Company Settings → File Categories now
relabels that entity type’s folders as well as its files.
2026-09-04 (Date values: one canonical shape; endorse deltas refuse the retired spelling)
Breaking change onPOST /api/v1/companies/{companyId}/policies/{policyId}/transaction/endorse.
A deltas value written to a Date field — at any depth, including a Date
nested inside a custom object or an embedded exposure — must be the canonical
{ "date": "YYYY-MM-DD", "timezone": "<IANA zone>" } object with both
members stated. Delta values are stored exactly as sent, and this channel never
converts, so:
- the retired
{ "day", "month", "year", "timezone" }spelling is refused with400/InvalidDelta, each refused value named indetails; - a write to a single member of a Date (
policy.policyEndDate.year) is refused with400/InvalidDelta— a Date is written whole.
- Documentation fix. The
Fmv1Dateschema now describes the value the API stores and returns:date(required) andtimezone. It previously declaredday/month/yearrequired and described a conversion toward that shape which no longer exists. Every example in the specification and in these pages now shows the canonical object.
2026-09-04 (generated-form bound key deprecated)
The bound response key returned by
GET /api/v1/companies/{companyId}/forms/generated is deprecated and now
always returns false. The key remains present for compatibility, so this is
not a breaking response-shape change and integrations do not need to change
immediately. Generated forms have one editable Working Document; issued
history is represented by immutable Packet Records rather than per-form bound
copies.
2026-09-03 (starter configuration seed surface retired)
Breaking change.POST /api/v1/companies/{companyId}/configuration/seed
has been removed. The authenticated seed/options and seed/generate paths now
return 501 with code starter_configuration_unavailable, and all three
operations have been removed from the published API specification. Start a new
company by posting a complete configuration document to
POST /api/v1/companies/{companyId}/configuration/import.
2026-09-02 (OFAC sanctions screening)
- Attribution.
screenedByon a screening, andreviewedByon each potential match, now record the External API service user when the screening or review was made with an API key. Previously they recorded the user who created the key. - Documentation fix. The
failureCategoryvalues areInvalidRequest,HttpError,NetworkError,Timeout,ProviderErrorandInvalidResponse; the earlier example showed a lower-casetimeoutthat the API never returns.
2026-09-02 (Google Sheets rating stages are no longer accepted)
Breaking change. Configuration documents no longer accept the retiredsegment-google-sheets or full-term-google-sheets rater types. Rating
spreadsheets now run only through Rating Service.
- Replace a segment Google Sheets stage with
segment-rating-serviceand a full-term Google Sheets stage withfull-term-rating-service. - Set
raterSpec.args.raterIdto the Rating Service rater UUID minted for the workbook. A Google Sheet ID is not a runtime rater reference. - Configuration validation rejects either retired enum value structurally; no grandfathering shim remains.
2026-09-01 (OFAC sanctions screening)
New endpoint.POST /api/v1/companies/{companyId}/ofac/screen runs an OFAC
sanctions screening for one exposure, saves the normalized result to it, and
returns it. Requires insured:update. See
OFAC Screening.
- Screening is advisory — it blocks no save, quote, bind, issue or import, and what it returns are potential matches for human review.
- The carrier turns it on by configuration. A screening runs through a
placed screening field (an
Object: OfacScreeningexposure field carrying the OFAC Screening input modality). A field that is not placed that way is refused with400and no provider call is made. - Sanctions lists, match threshold and provider are fixed platform
settings, recorded on each result. Sending
sourcesorminScoreis a400, not a silently ignored key. - A provider failure is a
200withstatus: failed, so the failure is recorded on the exposure rather than lost. - Every call is a fresh, billable screening. There is no idempotency key and no reuse of a recent result: a repeat replaces the stored one, review decisions included. Retrying is safe; retrying in a tight loop is expensive.
- Importing an exposure does not screen it. Screening request values sent to the entities API are stored like any other field data, with no provider call. Screening on import is deferred, not ruled out.
2026-08-31 (every quote must belong to a submission)
Breaking change. A quote must name a parent submission.referencingSubmission
is now required when you create a quote, and it must be the id of a submission that
already exists. A create that omits it, and an update that removes it, are rejected
with 400, problem code EntityInvariantViolation and inner code
quote-has-submission.
- Affected:
POST /api/v1/companies/{companyId}/entities/quoteandPATCH /api/v1/companies/{companyId}/entities/quote/{entityId}. - Create the submission first.
POST /entities/submission, then send its id asreferencingSubmissionon the quote. Nothing is minted for you: a create that arrives without a parent is refused rather than quietly given a generated wrapper submission, so a broken integration shows up as an error instead of as a book full of meaningless one-quote submissions. - Moving a quote is still allowed; unparenting it is not. Repointing
referencingSubmissionat another submission is a normal edit. Setting it tonullis not. - The rule holds from the submission side too. A
PATCH /entities/submission/{entityId}whosereferencingQuotesdrops a quote that no other submission takes on is rejected with409and codeSubmissionUpdateWouldOrphanQuotes, naming the quote ids. Moving a quote between submissions in one edit still works — what is refused is leaving it with no submission at all. - Submissions still never require quotes. The rule runs one way: a submission with no quotes is valid for its whole life, and deleting a submission is unchanged (its quotes must be dealt with first, as before).
POST /quotes/rate
and POST /quotes/{quoteId}/rate are reads, and a quote body with no
referencingSubmission rates exactly as it did before. Generating a quote from a
policy conversion is unaffected: the returned draft carries no submission, and the
requirement applies when you persist it. Reads, lists, exports, and deletes are
unchanged, including for any existing quote that has no submission.
2026-08-31 (file categories must be configured before they can be used)
Breaking change. A file’scategory must now name a category configured for
that file owner’s type. A write that introduces an unconfigured label is
rejected with 400, and the error lists the configured categories.
- Affected:
PATCH /api/v1/companies/{companyId}/files/{fileId}andPATCH /api/v1/companies/{companyId}/files/{fileId}/placements/{placementId}. - Matching is case-insensitive, and the stored value is the configured
spelling. Sending
loss runsagainst a configuredLoss Runssucceeds and storesLoss Runs. - Clearing is always allowed:
category: nullis never rejected. - Existing values keep working. The rule bars introducing an unconfigured label, not holding one: a file that already carries a label that is no longer (or never was) configured can be sent that same value again without error, so a client echoing back what it read does not start failing. Reading and filtering by such labels is unchanged.
- Categories are configured per owner type in Company Settings → File
Categories, and each owner type is an independent list. Read the current list
from
GET /api/v1/companies/{companyId}/files/categories?entityType={type}— the entries flaggedconfigured: trueare the writable ones. - If an owner type has no configured categories, no category can be set on its files until one is added.
2026-08-31 (documentation: both rate endpoints are reads — rating by id is not a mutation)
Clarification — no behaviour change. The Rating API docs now state the persistence contract in one place and without jargon, because “stateless” was being read as a property of the request rather than a promise about your data:POST /quotes/rateandPOST /quotes/{quoteId}/rateare reads. Naming a saved quote in the path does not make the call a write: no field is written, no rating run is recorded, and the quote row is byte-identical before and after. Both operations are now titled (Read-Only) in the API reference rather than (Stateless).- The rating results exist only in the response. Persisting them is your own
explicit second call —
PATCH /entities/quote/{entityId}, or a policyendorsetransaction for an in-force policy. That write-back is the only mutation in the flow, and it is documented under Persisting rating results. - A rate response reflects only the run that produced it: the rating workflow clears the containers it owns before any rater runs and rebuilds them from that run’s output. Rating containers you send are not echoed back, and a rate that prices nothing returns them empty — so writing such a response back wholesale clears previously stored figures.
2026-08-29 (seeding takes a starter program; the module/axis vocabulary is retired)
Breaking change. The starter-content module system is gone (#7996). Seeding
now names one starter program — a complete, standalone configuration — and
there is nothing to compose:
POST /configuration/seedtakes{ program?, replace? }. ThestarterSheetandmodulesfields are removed; a request that sends either is rejected by the route validator. Omitprogramto seed the product default (gl-pl). The error codesUnknownStarterSheet,UnknownStarterModule, andInvalidModuleSelectionare replaced by the singleUnknownStarterProgram.POST /configuration/seed/generatetakes{ program? }for the same reason.GET /configuration/seed/optionsreturns{ defaultProgram, programs }. ThedefaultStarterSheet,options, andmodulesproperties — and with them thegroup/alwaysOn/defaultSelectedaxis metadata — are removed.
premium, taxes, fees — and
billing mirrors them as three receivable lines plus one payable broker
commission, on either directBill or agencyBill. A freshly seeded company
carries no AutoSet targeting fullTermPricingInfo or fullTermBillingInfo, so
a stateless API rate returns complete pricing and billing with no browser
involved.
2026-08-28 (the generated tier is retired; x-generated removed)
Behaviour change. No company configuration declares the generated field
tier any more, and the machinery behind it is deleted:
- The entity configuration schema no longer publishes
x-generated. Every survivingreadOnlyfield means the same thing: a value you send is silently ignored and the server-managed value wins — no field rejects a supplied value with400 GeneratedFieldWriteany more. The fields that used to carry the mark are ordinary calculated fields now; read theirx-variant/x-puritymarks instead. - The configuration import no longer accepts a
generatedproperty on a field or sub-field. Sending one is rejected with a message pointing at the replacement:recalculate: "WHEN_UNSET"(withclientValueAllowedas needed) for run-once values,termInvariantfor term freezing.
2026-08-27 (policy-list pagination is zero-based and caller-sized)
Breaking correction.GET /api/v1/companies/{companyId}/policies/list
now uses the same pagination contract as entity lists: zero-based pageNumber
plus optional pageSize (default 50). The retired one-based page parameter is
no longer part of the public contract; callers should send pageNumber=0 for
the first page, pageNumber=1 for the second, and so on.
2026-08-27 (eventStatus is system managed; open/close history is replaceable)
Behaviour change.Event.eventStatus is now a system managed field on
every company. A value sent for it on a generic create or
PATCH /entities/event/{entityId} is silently ignored and the stored value
is left unchanged — a 200, not an error.
- Previously such a
PATCHreturned409(GuardedStatusFieldWrite), and an expliciteventStatuson create was honoured. Both are gone: an integration that relied on the create behaviour to load already-closed claims must now supply the event’seventOpenCloseHistoryinstead, from which the platform derives the status and close date. - Unchanged: the status still moves through
POST /events/{eventId}/closeandPOST /events/{eventId}/reopen. quoteStatusandsubmissionStatusare unaffected — they remain guarded and still answer409on a disallowed generic move.
PUT /api/v1/companies/{companyId}/events/{eventId}/open-close-history
replaces an event’s entire open/close-history log in one call, for historical
imports and corrections. The log must start with open and alternate
close/reopen, with non-decreasing effective dates; the event’s status and
close date are re-derived from its last entry.
2026-08-26 (smart-tag audit recognizes tenant-defined smart tags)
The read-only smart-tag audit endpoints (GET /forms/template/{number}/smart-tag-audit and
GET /forms/generated/{id}/smart-tag-audit) now judge hashed anchor
identities against the company’s fields and tenant-defined smart tags.
Previously a hashed anchor whose identity came from a smart tag with no
same-named field was misreported as dead-hashed even though document
generation resolves it; such anchors now correctly report resolvable.
No shapes changed — only classification accuracy.
2026-08-26 (policy segments name the transaction type that wrote them)
Additive. Every policy segment now carries atransactionType: one of
NEW_BUSINESS, ENDORSE, CANCEL, REINSTATE, RENEW, or null.
- Where: the
segments[]array of every policy version response — the five transaction endpoints (POST /policies/transaction/new-business,POST /policies/{policyId}/transaction/endorse,/cancel,/reinstate, andPOST /policies/transaction/renew),POST /quotes/bind, andGET /policies/{policyId}/versions/{version}— and thesegments[]of each item returned byGET /policies/list,GET /policies/versions, andGET /policies/{policyId}/versions(those three include segments only withdetail=full). - What it means: a transaction rewrites the FULL segment set at its own
policy version, so this is the version’s transaction type, not a per-segment
edit marker. A renewed term’s segments read
RENEW. An endorsement’s version readsENDORSEacross its whole span — including the part the endorsement did not edit — while the versions beneath it keep the type that wrote them. nullmeans UNKNOWN, not new business. The segment was written before this member existed. Existing segments are not backfilled, so treatnullas “no answer” rather than inferring a type from it.
2026-08-26 (BREAKING: bound quotes refuse edits; the entity list rejects unknown query parameters)
Two breaking corrections on the Entities API. Both replace a request that used to succeed with a request that now fails loudly — in each case the old success was returning or accepting the wrong thing silently, so a caller had no way to notice. Read them as errors arriving where wrong data used to.Changed: editing a bound quote is refused with 409 QuoteBoundImmutable
PATCH /entities/{entityType}/{entityId} on a quote whose quoteStatus is
already bound used to accept the edit and answer 200. The bound policy never
saw it: a policy’s values live in its own segments, written once by the bind, and
nothing re-reads the quote afterwards. Every accepted edit therefore only widened
the divergence between the quote and the policy it is supposed to describe, with
nothing left to reconcile them.
Such a request is now refused with HTTP 409 and code QuoteBoundImmutable,
whatever the body holds. The error carries referencingPolicy, so the policy
id is available from the error body without a second call.
The two remedies the refusal names:
- To change what is in force, endorse the policy —
POST /policies/{policyId}/transaction/endorse. - To model a variation, create a new quote (or copy the bound one).
update_entity tool reaches the app over this same route, so it is covered too.
Changed: the entity list rejects unknown query parameters and range-checks paging
GET /entities/{entityType}/list and the bare GET /entities/{entityType}
collection route used to ignore any query parameter they did not declare.
Unknown names are now rejected with HTTP 400 and code InvalidRequest, and the
message lists the keys the endpoint accepts: filterText, filters,
pageNumber, pageSize, sortBy, sortDirection.
Paging is why this is a correction rather than a tightening. The list’s only
paging control is pageNumber (zero-based) plus pageSize, and both
were applied correctly all along — but every other spelling a caller reaches for
(page, limit, offset, skip, cursor) was silently dropped, so the
endpoint answered 200 with page 0 and hasMore: true every single time. A
page=N loop re-read the first page forever and never failed, which means a book
larger than one page could not be walked at all, and a sweep taken under it
looked complete when it was not.
pageNumber and pageSize are now also range-checked against the bounds the
OpenAPI spec has always published — pageNumber a non-negative integer,
pageSize a positive integer. Neither was previously enforced: a negative or
fractional value reached the database as a raw LIMIT/OFFSET and surfaced as
an unmapped 500, and pageSize=0 was worse than an error — 200 with an empty
page and hasMore: true, a second walk that never advances. Both are 400s now.
Action required: replace any non-pageNumber paging parameter with
pageNumber / pageSize. Neither the default page size (51) nor the response
envelope changed, so a caller that does not paginate — or that already pages with
pageNumber — is unaffected.
2026-08-26 (BREAKING: Financials Config Document v1 removes policy component kinds)
Breaking correction to Financials Config Document v1: policy invoice line item types now contain onlyname, direction, and deprecated.
policyFinancials.invoiceTypes[].lineItemTypes[].componentKind and
policyFinancials.classifications[].legacyMatch were removed from export and
are rejected by import. The document remains documentVersion: 1; previously
exported documents containing either retired key must remove it before import.
The policy line-item configuration writers likewise reject their former
optional kind input. The category-level kind (operating | reserved) is a
different live field and is unchanged.
2026-08-26 (rater failures now return structured, located diagnostics)
The production raters now report their failures as structured diagnostics instead of raw HTTP errors or silent skips, on both stateless rate endpoints (POST /quotes/rate and POST /quotes/{quoteId}/rate):
- Several failures that previously returned HTTP
5xxnow return HTTP200withseverity: errordiagnostics and nodata— rating vendor failures (already200since 2026-08-25, now also carrying alocation), quote-data faults a rater rejects (e.g. a required rating input that is present but not numeric), and spreadsheet outputs that never settle. - Failures that previously rated successfully with silently missing
outputs — an errored workbook output cell, a mis-wired rating stage
(bad
outputPathor missing rater args), an unbuildable InsCipher tax plan — now fail the rate withseverity: errordiagnostics naming the cause. - Per-output-field skips (values a rater produced that could not be written
to the quote) now ride beside
dataasseverity: warningdiagnostics.
data field, never by the mere
presence of diagnostics.
2026-08-26 (rate responses can carry warning diagnostics beside data; diagnostics gain an optional location)
Additive changes to the diagnostics array on the two stateless rate
endpoints (POST /quotes/rate and POST /quotes/{quoteId}/rate):
- A successful rate can now return
severity: warningdiagnostics alongsidedatawhen a rater reports them. A clean rate’s response is unchanged (nodiagnosticskey). As before, the failure signal is the absentdatafield, never the mere presence ofdiagnostics. - Each diagnostic can carry an optional
locationobject naming where in the rating workflow it arose:stageIndex,raterDebugName, an optionaltargetPath, and the ratedsegmentWindows(ISO-8601 date-time instants). It is present when a rater reported the diagnostic mid-run; failures outside any one stage (e.g. a rating vendor transport failure) stay location-free.
2026-08-25 (portable Financials Config Document v1)
AddedPOST /financials/config/export and POST /financials/config/import.
Export returns the complete natural-key-only financials configuration document;
import additively creates missing vocabulary and applies reporting frames. The
document round-trips directly, import is idempotent, and ?dryRun=true reports
the same validated plan without financials-config writes. The endpoints require
company.configuration:export and company.configuration:import, respectively.
2026-08-25 (configuration retirement memory uses three explicit identity flavors)
Configuration export and schema responses now describe the server-derivedretirementIdentities slice. Each record is exactly one of field,
customObjectSubfield, or optionSetValue, with its address and nested previous
signature. The slice is omitted when empty. Imports may echo the slice, but the
server does not trust submitted retirement memory; it derives the next value from
the previous immutable configuration version.
Legacy retiredFields request bodies remain accepted during the coordinated
Control Plane and tenant-mirror migration, but new exports use
retirementIdentities.
The configuration metadata contentHash and import expectedContentHash
precondition now identify the canonical body returned by configuration export.
For an immutable snapshot that still stores legacy retiredFields bytes,
metadata hashes its projected retirementIdentities body; a stale-import 409
returns that same projected hash as currentContentHash. Callers can therefore
use metadata’s token with an exported body throughout the compatibility window.
2026-08-24 (rating failures on the rate endpoints return 200 with diagnostics instead of an HTTP error)
Breaking change to how the two stateless rate endpoints report a failed
rate:
POST /api/v1/companies/{companyId}/quotes/ratePOST /api/v1/companies/{companyId}/quotes/{quoteId}/rate
4xx/5xx (such as a 502 with code
inscipher-tax-calculation-failed) — now returns HTTP 200 with a
diagnostics array ([{severity, code, message}]) and no data. The
absent data field is the failure signal.
What is unchanged:
- Invalid requests keep their status codes: create-quote validation failures,
missing/unknown
ratingWorkflowName, missing rating targets, and unsupported quote types are still400; an unknown quote id is still404; no configured workflows is still422. - Unexpected application faults are still
500. - A successful rate still returns
data(with nodiagnosticstoday).
data
field instead. This also affects retry logic that keys off 5xx responses.
Do not treat the presence of diagnostics alone as failure: future versions
will also return severity: warning diagnostics alongside a successful
data.
2026-08-24 (rating workflows can declare preconditions; unmet preconditions fail rating with a structured 400)
Additive. A rating workflow’s configuration can now declare preconditions — boolean conditions over the quote’s field values, each with a message. When any precondition of the selected workflow is unmet, both rate endpoints (POST /quotes/rate and POST /quotes/{quoteId}/rate) reject
the request with a 400 carrying error code
rating-workflow-preconditions-unmet; userMessages lists every unmet
precondition’s configured message verbatim, in configuration order. Workflows
that declare no preconditions are unaffected.
2026-08-20 (the bordereau export goes async: CSV download and Google Sheets export are replaced by export runs)
Breaking change, zero known consumers. The two synchronous bordereau export endpoints are removed and replaced by an asynchronous export-run family that produces the FULL bordereau — no row cap, no paging — as a background job.Removed: GET /policies/bordereau/download and POST /policies/bordereau/export
The synchronous CSV download and Google Sheets export are retired outright —
no deprecation window. Both were capped at 50,000 rows; their replacements
stream the whole result set.
Added: the bordereau export-run family
POST /policies/bordereau/export-runs— start a run. The body is the same bordereau export request the retired endpoints took (periodStart/periodEnd,actions,filters,sortBy/sortDirection, and the orderedcolumnsgrammar), minuslimit/offset— a run always exports every matching row. Returns{runId, outcome}immediately; duplicate starts collapse onto the run already in flight (?force=trueto skip), and the per-company limit of 3 runs in flight is shared with entity export runs.GET /policies/bordereau/export-runs/{runId}— poll the run’s status (queued→running→succeeded|failed, andexpiredonce the file passes its retention horizon).GET /policies/bordereau/export-runs/{runId}/download— download a succeeded run’s file as CSV (?format=csv, the default) or an Excel workbook (?format=xlsx), namedbordereau-export-{runId}.POST /policies/bordereau/export-runs/{runId}/drive— deliver a succeeded run’s file to Google Drive as a new spreadsheet. The body keeps the retired Sheets endpoint’s contract: the caller suppliesfolderIdand their owngoogleOAuthToken, and the spreadsheet is created as that user. (Writing into an existing spreadsheet viaspreadsheetId, and the customspreadsheetName, do not carry over — each delivery creates a fresh spreadsheet namedbordereau-export-{runId}.)
GET /policies/bordereau (the paged JSON list) is unchanged and remains
synchronous. The required permission everywhere is company.policy:read,
exactly as before.
2026-08-20 (the legacy pricing vocabulary is removed as input; classification is required for every company)
Removed: group / kind / earningBasis as pricing-component input
The legacy pricing-component vocabulary is retired as input, for all
companies. This completes the deprecation announced on 2026-08-19 and
supersedes its per-company timeline — there is no longer an “until your company
is migrated” phase.
- Stated values are ignored and discarded. A
group,kindorearningBasisyou send is never validated and never stored on a newly written component. The dual-agreement checks are gone along with the derivations they policed — a stated legacy field cannot conflict with anything. - Echo tolerance. A client that GETs a stored component still carrying the legacy keys and POSTs it back keeps working: the retired fields are simply dropped on write.
- Responses. A component written from now on carries only
{label, value, classification, qualifier, earningSchedule}. Components stored before this contraction keep echoing the legacy keys until the per-tenant data sweeps remove them — treat them as historical output, never as identity, and do not rely on their presence.
Changed: classification is required on every pricing component, for every company
Every pricing component submitted anywhere on the API must carry a
classification naming a live entry in your company’s classification
registry — unconditionally. The per-company ratchet described in the 2026-08-19
entry (“once your company is migrated”) is superseded: the requirement now
applies to every company. This covers every write that accepts pricing
components: the policy transactions (new-business, renew, endorse,
cancel, reinstate), entity create and update bodies carrying
fullTermPricingInfo, and POST /quotes/rate.
label(string) andvalue(number) remain required;qualifierstill defaults to"",earningSchedule(pro-rata|immediate) still defaults topro-rata, and<classification, qualifier>must be unique within a contract.- An absent or null
fullTermPricingInfostays valid everywhere. A pricing-free submission never enters these rules. - A brand-new company must configure its classification registry first: a pricing component sent while the registry has no entries is refused with a message saying exactly that.
- Refusal codes are unchanged:
400withInvalidPolicyDataonnew-business/renew, andInvalidFieldModelV1Dataonendorse/cancel/reinstateand the entity and rating doors. - The four rollups (
premium,taxes,fees,total) are now summed classification-first: each component reports into the rollup its registry entry names. They remain server-computed and read-only, and any rollup value you send remains ignored and recomputed.
Removed: the v1 <group, label> pricing-vocabulary check and the policy-financials flag
The v1 rule that required every pricing component on an aggregate-less priced
policy to resolve to live billing vocabulary by <group, label> is deleted —
finv2-policy-pricing-vocabulary refusals no longer exist, and a policy with
zero billing configuration still prices and earns. The Billing Aggregate door
is unchanged: a stated aggregate must still satisfy the receivable checksum
(finv2-policy-billing-checksum) and the per-line vocabulary and direction
rules (finv2-policy-billing-vocabulary), and these now apply unconditionally
— the policy-financials rollout flag that once gated them is retired (Policy
Financials V2 is the platform’s only behavior). The separate
policy-financials-invoicing flag still gates invoice creation.
2026-08-20 (validation findings use structured field paths only)
Removed: legacy field strings from validation findings
Violation objects returned by both entity batch validation and data-validation
run findings no longer include the legacy nullable field string. Read the
required fieldPath array instead:
GETorPOST /companies/{companyId}/data/{entity}/validateGET /companies/{companyId}/configuration/data-validation-runs/{runId}/findings
['coverages', '0', 'limit'] identifies a nested list value, while
[] identifies a record-level violation that names no single field. Do not derive
a dotted/bracket string from these segments; use the array as the locator.
2026-08-20 (tenant list filters are declared explicitly)
Changed: implicit field-derived list filters are no longer accepted
Tenant filters on entity lists, policy list/version reads, bordereau reads and exports, and entity export runs must now use:key names a definition in the company’s listFilters configuration for the
surface being queried. The definition supplies the expression, logical result
type, and permitted operators. Explicit fixed system filters (systemUser,
systemDate, and systemId) remain supported.
Implicit tenant-field forms such as text, number, optionSet, and join
now fail with HTTP 400 and code implicit-filters-disabled. There is no
compatibility form; replace each field-derived request with its declared
definition key.
2026-08-19 (the legacy pricing vocabulary is deprecated; classification becomes required on migrated companies; the commission rollups are retired)
Removed: the brokerCommission / programCommission rollups on fullTermPricingInfo
The two commission rollups are retired from the pricing contract. The
platform no longer computes them, and they no longer appear on newly written
policy versions, quotes, or rating results. They were never writable — any
value you sent was already ignored — so no request shape changes.
- Why: commission is money owed by the carrier, not part of the policy’s
price. Its home is the Billing Aggregate (
fullTermBillingInfo), where a commission obligation is a payable line — not a pricing rollup. - Components are unaffected.
pricingComponentsof kindBrokerCommissionorProgramCommissionremain accepted and stored exactly as before; their amounts simply report into no rollup. They were already excluded fromtotalby definition, sototaldoes not move. - Reads of old data: a policy version stored before the retirement may still return the two keys until its data is migrated. Treat them as historical output; do not rely on their presence.
premium,
taxes, fees, and total.
Deprecated: group / kind / earningBasis on a pricing component
The legacy pricing-component vocabulary is now deprecated. This supersedes
the 2026-08-18 entry’s statement that a component written as
{label, group, kind, value[, earningBasis]} “stays valid indefinitely” — it
no longer does. The timeline is per company:
- Until your company is migrated to Policy Financials V2 (its classification registry has no entries yet), nothing changes: legacy-only components are accepted and pass through untouched, exactly as before.
- Once your company is migrated (its classification registry is
configured), every pricing component you submit anywhere on the API must
carry a
classification. A legacy-only component — one with noclassification— is rejected with a400whose message names the migration and the component. This applies to every write that accepts pricing components: the policy transactions (new-business,renew,endorse,cancel,reinstate), entity create and update bodies carryingfullTermPricingInfo, andPOST /quotes/rate.
classification as long as they agree with the
derivations documented under
Pricing Component Identity.
Responses are unchanged for now. Stored components carry both vocabularies,
and responses keep echoing kind / group / earningBasis as deprecated
compatibility output. A future release removes the legacy fields from requests
and responses entirely; move readers onto classification /
qualifier / earningSchedule ahead of it.
The rollups are unaffected by this deprecation: the four (premium, taxes,
fees, total — see the retirement entry above) remain server-computed and
read-only, and any rollup values you send remain ignored and recomputed rather
than rejected.
2026-08-18 (classified pricing, a total rollup, and a billing container beside the pricing one)
Added: classification / qualifier / earningSchedule on a pricing component
Each element of fullTermPricingInfo.pricingComponents accepts three new
optional fields. Nothing about the existing shape changes: a component
written as {label, group, kind, value[, earningBasis]} stays valid
indefinitely and passes through untouched.
classification(string) — a key naming an entry in your company’s classification registry, a new per-company financials configuration surface. It must name a live entry. Registry keys are lower-kebab-case (^[a-z0-9]+(?:-[a-z0-9]+)*$, at most 64 characters) — e.g.surplus-lines-tax. This is the stable identity half.qualifier(string) — the structured qualifier that completes the identity, typically a jurisdiction. Defaults to"".earningSchedule(pro-rata|immediate) — the successor ofearningBasis. Omitted meanspro-rata.
<classification, qualifier> is the component’s identity, replacing
<group, label> — which is the point of the change. The taxing jurisdiction
stops being baked into a display string, and renaming a label becomes a purely
cosmetic edit instead of the creation of a new charge.
Three component shapes are accepted, all valid during this period:
A V2-carrying component is normalized at the write door:
qualifier defaults
to "", earningSchedule to pro-rata, and any absent legacy field is derived
— kind from the registry entry’s rollup (premium → Premium, taxes →
Taxes, fees → Fees), group → "Policy Invoice", and earningBasis →
fully-earned-at-inception when the schedule is immediate (otherwise
omitted). Because the stored component carries both vocabularies, a reader on
either one keeps working — send classification and the response still shows
you a kind.
What is refused (400): a classification that names no registry entry or
names a deprecated one; an unrecognised earningSchedule; a dual component
whose legacy field disagrees with the derivation (the error names the component
and both values); two components sharing one <classification, qualifier> pair;
and any V2-carrying component sent by a company whose classification registry
has no entries at all — configure the registry first.
Added: total, a sixth rollup on fullTermPricingInfo
total is premium + taxes + fees — the grand total of what the insured owes.
The commission rollups and Other components are excluded from it by
definition, not by omission: commission is money owed by us, and Other feeds
no rollup at all. Like the existing five, it is computed by the platform on
every write, is always present on a response, and ignores any value you send.
It is addressable as a bordereau field-column path (fullTermPricingInfo.total).
total does not reinstate policyGrandTotal, which was retired from this
container on 2026-08-05 with no successor; total is a new rollup with the
definition above. The policyGrandTotal on the rating-result container is a
separate field and is unchanged.
Added: fullTermBillingInfo — the term-level Billing Aggregate
A new container sitting beside fullTermPricingInfo: what a policy owes, per
line item, free of invoices.
invoiceType and lineItem are required, non-empty strings; direction is
receivable (owed to us) or payable (owed by us); amount is a required,
finite, signed number — a price refund is a negative receivable. lines
itself is nullable, because a policy with no billing configuration still prices
and earns. A line’s identity is the pair <invoiceType, lineItem>, unique
within one container; a duplicate pair is a 400.
You author this container, with one exception: on a cancel or reinstate the
platform derives it when you omit it. It deliberately carries no classification
and no rollup, exactly as fullTermPricingInfo carries no invoice type and no
line item: pricing and billing keep disjoint vocabularies, and per-rollup cash
stays a naming convention over line items rather than schema.
Where it goes. It is accepted everywhere fullTermPricingInfo is accepted
as a request key — inside data on new-business and renew, and as a
top-level sibling channel on endorse, cancel and reinstate — and returned
everywhere fullTermPricingInfo is returned: hoisted next to it on the policy
version response, the version lists, the policy list item summary, and the
transaction responses. Like its sibling it is a reserved full-term container, so
policy.fullTermBillingInfo is rejected in a deltas path at any depth; it has
its own channel. It does not by itself satisfy the endorse “at least one
channel” requirement.
Structure is validated on every request — shape, direction membership,
finite amounts, and <invoiceType, lineItem> uniqueness. Two further laws
apply once policy financials is enabled for your company, on every persisted
version that states an aggregate:
- The receivable checksum. The
receivableamounts sum exactly tofullTermPricingInfo.total, refused otherwise with a400and the codefinv2-policy-billing-checksum, which carries the gap in cents as pricing − receivable. This is also the endorsement restate-or-refuse gate: a transaction that moves the price without restating the lines fails it. - Per-line vocabulary and direction. Each
<invoiceType, lineItem>names a live policy invoice type and line item type, and thedirectionyou send must be the one that line item is configured with — a checked assertion, not an input. Otherwise a400with the codefinv2-policy-billing-vocabulary, which names every offending line rather than the first.
lines is
present, empty array included — so "lines": [] beside a non-zero price is a
checksum refusal, not an exemption.
What changes for you. Every V1 request shape remains valid — the three
component fields are optional, total is read-only, and fullTermBillingInfo is
a new optional key. Adopt them when your classification registry is configured.
The two billing laws above bind only once policy financials is enabled for your
company and you state an aggregate, so a request that sends no
fullTermBillingInfo behaves exactly as it did before.
2026-08-18 (export runs — background CSV exports of any entity type)
Added: POST /entities/{entityType}/export-runs, GET .../export-runs/{runId} and GET .../export-runs/{runId}/download
Three endpoints that export the FULL filtered result set of one entity type as a
CSV file produced in the background. They
cover every exportable entity type (event, exposure, quote, submission,
person, organization, and policy), each gated on the same per-type
permission required for that entity (company.{entity}:list; Policy uses
company.policy:export).
The start endpoint takes the export request as an optional JSON body — the
same fields / filters / filterText / sortBy / sortDirection / asOf
vocabulary used by entity exports; omit the body (or
fields) to export the type’s full default column set. It returns
{ runId, outcome } immediately and snapshots both the request and the company’s
current configuration version, so a configuration import landing mid-run cannot
change the file. Poll GET .../export-runs/{runId} for
queued → running → succeeded / failed, then download the CSV — streamed
as a text/csv attachment — once the run succeeds.
Four behaviours to build against. Starting a run whose request matches one
already in flight returns that run’s id with outcome: "duplicate" rather than a
second export — a successful 200, not an error; pass ?force=true to start a
new run anyway. A company may have 3 export runs in flight at once: a 4th
distinct request is refused with a 429 whose body carries inFlightRunIds, and
?force=true does not lift that limit. The parked CSV expires: after the
run’s expiresAt (30 days from start) the file is deleted, the run polls as
expired, and the download answers 404 — the run’s facts stay readable, and a
fresh export produces a fresh file. And the download serves only a
succeeded run: every other status is a 404 whose reason the poll reports.
2026-08-18 (Policy on the Entities API now answers with the API to use instead)
policy / policies on /entities/{entityType} returns a pointer to the Policy Transactions API
Policy has never been one of the Entities API’s CRUD types — listing, reading,
and writing policies live on the Policy Transactions
API, and listing specifically on List
Policies
(GET /api/v1/companies/{companyId}/policies/list). Calling the entity routes
with policy or policies was already rejected; the rejection just said
Invalid entity type, which read as a spelling problem rather than a
wrong-API one.
Those calls now come back with a dedicated message naming where to go:
400, and it applies to every CRUD entity route — list
(/entities/policy and /entities/policy/list), create, get, update, delete,
/configuration, and /deleteAll — matched case-insensitively on both the
singular and plural spelling. Nothing that used to succeed changes: the entity
export-runs routes continue to accept policy, and every other entityType keeps
the generic INVALID_ENTITY_TYPE error.
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.
