Skip to main content

November 13, 2026

A moved payee default is caught without a billing change. Breaking for callers that send entityUpdateOptions.invoices: "generate". An update with the option, Complete included, answered unchanged whenever the quote’s plan followed its billing and saved settings, even after a field a payee default reads changed, such as the quote’s broker. It now answers unchanged only when the plan’s payee defaults still hold too. A default that moved to a payee other than the one the quote pays is 409 finv2-quote-invoices-need-force naming applyChangedDefaults, with the change in changedSettings; send invoicesForce: ["applyChangedDefaults"] to regenerate paying the new default. A default that no longer resolves, for an invoice type with something to invoice, is 409 finv2-quote-invoice-settings-missing. An update without the option still reports unchanged. See Keeping invoices up to date. A quote’s invoices follow its billing, on request. Breaking for some refusals, for companies that invoice. Any Update Entity of a quote now takes entityUpdateOptions.invoices: "generate", not only the update that completes it: once the update is saved, a quote with no plan generates one, a plan that no longer follows the quote’s billing is regenerated, and a plan that does stays as it is. A quote whose intended layer is not invoicing keeps no invoices. Its response, and the response of every quote update that changes the quote’s billing, carries an invoiceReport (outcome: generated, regenerated, unchanged, outOfDate or noInvoices; replacedHandEdits; changedSettings). Two new 409s save nothing from the request, the update’s field changes included: finv2-quote-invoice-settings-missing when no payee can be worked out for an invoice type (missingSettings), and finv2-quote-invoices-need-force when regenerating would replace invoices edited by hand in the app, or a payee whose payee default moved (forcesNeeded, handEdited, changedSettings). Send the update again with entityUpdateOptions.invoicesForce: ["replaceHandEdits", "applyChangedDefaults"], or the ones named, to regenerate anyway. Generate Quote Invoices takes the same forces in its body ({ "force": [...] }), returns the same 409s and adds invoiceReport to its response; it still always regenerates. Payees now follow their payee default: generating remembers the default each invoice type computed and, when it moves (for example a new broker), pays the new default instead of the payee the quote held, where before the quote’s plan or earlier payee was kept; once a quote has generated with its defaults remembered, a payee you chose holds until its default moves, and on a plan saved before that, the first regeneration asks once (applyChangedDefaults) before a differing payee gives way to its default. A type whose invoices name more than one payee waits for you to choose one. A bind with invoices: "generate" applies the same rule without asking: a payee never matched to a default is kept, and a default that moved replaces it. What changes for existing callers: Complete with invoices: "generate" can now return the two 409s, and keeps a plan that already follows the billing instead of regenerating it; a missing payee at Complete or Generate Quote Invoices returns 409 finv2-quote-invoice-settings-missing instead of 422 finv2-payment-generation-refused; invoices on an update that does not complete the quote is accepted instead of 400 INVALID_COMPLETE_INVOICES, and on a quote already complete instead of 409 quote-already-complete; and Generate Quote Invoices on a quote whose intended layer is set to one other than invoicing now generates nothing (noInvoices). See Keeping invoices up to date.

November 12, 2026

Turn on a company’s email branding. Additive. Get Company Email Branding and Set Company Email Branding (GET and PUT /email-branding) read and change whether the emails AI Insurance sends for a company carry its logo, primary colour and name ({ "enabled": true }) or AI Insurance’s look ({ "enabled": false }). The setting is off for every company until someone turns it on, in the API or in Company Settings → Branding. It covers automatic emails, such as member invitations, and emails sent from email templates; the wording of an email does not change. Emails sent from email templates, which carried the company’s branding until now, carry AI Insurance’s look while the setting is off. Reading requires company.view; changing it requires company.edit. Both answer 404 while email is not turned on for the company. Read every event’s reserve updates in one feed. Additive. List Company Reserve Updates (GET /financials/reserve-updates) returns the reserve-update feed of every event: each row of List Reserve Updates plus the event it belongs to (linkedEvent, eventReferenceId) and, on user and erodingPayment rows, the running reserve (runningReserveCents). These are the rows of the app’s company-wide Reserve Updates export. Filter by categoryId, read the feed as of a date with asOfDate, and page with limit and cursor. It requires event.view. Deliveries name their automatic email. Additive. A message’s delivery on Get Record Email Conversation and the policy conversation endpoint now carries automaticEmail: the automatic email it was (key and name) and source, either { "kind": "default" } for our default wording or { "kind": "company", "version": N } for the company’s version. template is now null for an automatic email, so it only ever names an email template version you can read. Every other delivery has automaticEmail: null. Order a Join picker’s options. Additive. A Join Picker or Pointer Picker field location can set inputModalityArgs.sort to { "field": …, "direction": "asc" | "desc" }, where field is createdAt, updatedAt or a field of the listed entity. The picker lists every matching option in that order, after its filters and the user’s search. Without it, the order is unchanged. Import Configuration and Validate Configuration refuse an unknown field or direction. See Join picker order. Changing an approved invoice takes approval authority. Breaking. On an invoice that is approved and requires approval (approved and approvalRequired both true), Update Invoice, Re-link Invoice, Change Invoice Payee, Void Invoice, Restore Invoice, Delete Invoice, and an Update Policy Invoices plan that voids it, now also require invoice.approve, the permission that approves it. A key without it receives 403. Unapproved invoices, invoices that do not require approval, and payment writes are unchanged. See Financials permissions. Voiding follows the invoice’s stage. Breaking for custom roles; existing keys keep their access. invoice.void is replaced by invoice.void-finalized and invoice.void-paid. Void Invoice and voiding through Update Policy Invoices require invoice.void-finalized: both still refuse an invoice with live payments. invoice.void-paid lets the app’s Void remove a paid invoice’s payments and void it without payment.delete. Every existing role holding invoice.void now holds invoice.void-finalized, and also invoice.void-paid if it held payment.delete. Update any role definitions you manage outside the app to the new names. Rating diagnostics name their target. Additive. A diagnostic’s location on POST /quotes/rate and POST /quotes/{quoteId}/rate can now carry targetLabel and targetEntityType beside targetPath when the rated target is a record such as an exposure: targetLabel is its display name, or its record type and 1-based position (Exposure 3) when it has none, with (#N) added when two targets share a name. The quote itself and custom-object targets carry no label. See Rating failures. Preserve a record’s legacy activity. Additive. Replace Record Legacy Activity stores a migrated record’s activity feed from the legacy system as that system displayed it, and the record’s Activity tab shows it under Before migration. The request replaces the record’s preserved feed, so a migration can be rerun. It requires the record type’s import permission, and note.view-privileged for an entry marked privileged. See Legacy activity. Broker Portal branding moved to Company Settings. Breaking for configuration documents that set brokerPortal.branding. A company’s logo and brand colours are now company settings, used by its emails and its Broker Portal, so the configuration no longer carries them. Import Configuration and Validate Configuration refuse brokerPortal.branding as an unknown key, export no longer includes it, and Patch Configuration no longer accepts set-broker-portal-branding. brokerPortal no longer requires any key, and set-broker-portal-decline-copy-to, set-broker-portal-decline-forms and set-broker-portal-assignable-users now create the section when the configuration has none. Remove branding from configuration documents you keep outside the app. See Broker Portal section. Date values accept the optional boolean importedAsUnknownTimezone. CALENDAR_DATE sets it when it uses the Eastern fallback because no source timezone was supplied. The flag is preserved through saves and date arithmetic; explicitly selecting a timezone in the date picker clears it. Existing dates remain unchanged. Send email from a submission. Additive. A submission can now send email from a template, like a claim, insured, quote or policy. List Record Sendable Email Templates, Preview Record Email and Send Record Email accept submission as the {entityType}, and need email.send and submission.view. See Sending. Invoice permissions follow the invoice’s stage. Breaking for custom roles; existing keys keep their access. invoice.create, invoice.edit and invoice.delete are replaced by one permission per stage — draft, finalized (owed, no_charges), paid (partially_paid, paid) and void (voided):
  • Create: invoice.create-draft, invoice.create-finalized, and the new invoice.create-paid.
  • Edit: invoice.edit-draft, invoice.edit-finalized, invoice.edit-paid and invoice.edit-void. invoice.edit-paid no longer needs invoice.edit alongside it.
  • Delete: invoice.delete-draft, invoice.delete-finalized, invoice.delete-paid and invoice.delete-void.
  • Restore Invoice now requires invoice.restore, split from invoice.void.
  • Finalize Invoice on a draft whose draftPayments post requires the new invoice.finalize-paid, or invoice.finalize and payment.record.
  • Re-link Invoice with movePayments now also requires payment.delete and payment.record, as removing and re-recording the payments yourself would.
  • Update Policy Invoices requires invoice.void for a plan that voids invoices.
A write is allowed in one request when the key could reach the same result in several, so invoice.create-finalized and payment.record together still allow what invoice.create-paid allows. A refused write returns 403 naming the permissions that would allow it. See Financials permissions. Every existing role was migrated: invoice.create became invoice.create-draft and invoice.create-finalized (plus invoice.create-paid with payment.record); invoice.edit became invoice.edit-draft, invoice.edit-finalized and invoice.edit-void, keeping invoice.edit-paid only alongside invoice.edit; invoice.delete became invoice.delete-draft, invoice.delete-finalized and invoice.delete-void (plus invoice.delete-paid with invoice.edit-paid); invoice.finalize gained invoice.finalize-paid; and invoice.void gained invoice.restore. Update any role definitions you manage outside the app to the new names.

November 11, 2026

Send email from a record. Additive. A record’s email can now be sent through the API, as Send email does in the app, from a claim, insured, quote or policy, or from one of its packets. List Record Sendable Email Templates lists the templates the record can send, Preview Record Email renders one, and Send Record Email sends it; it takes an Idempotency-Key header, so a retried request sends once. Follow the send’s delivery.status on Get Record Email Conversation, retry a failed one with Retry Record Email Delivery, and list the emails sent from the record’s packets with List Record Packet Emails. Each has a /policies/{policyId}/… twin. Sending needs email.send and view on the record. See Sending.

November 10, 2026

Correct Invoice Numbers after posting. Update Invoice now accepts invoiceNumber changes on finalized invoices. Omit it to keep the current value, send null or blank to clear it, or supply a trimmed value of up to 255 characters. Policy invoices permit number-only corrections with all other document fields unchanged. These corrections are journaled without changing the Transaction ID, balances, payments, or approval. Existing edit permissions, including invoice.edit-paid for paid invoices, still apply. Import PDFs directly onto records without templates. Additive. Import Historical Form Packet attaches historical issued PDFs to quotes and policies, preserving the original issuance date and document order. Stable caller-generated packet ids make identical imports safe to retry across import runs. Upload Event Form attaches a PDF as a working form to a claim or incident; it creates no packet. Event uploads support the Idempotency-Key header with a 30-day retry window. Both require form.create and the target record’s view permission, and accept readable, unencrypted PDFs up to 20 MB. See Import historical documents. An email’s delivery shows when it is waiting for its packet. Additive. A delivery on List Record Email Conversations and Get Record Email Conversation now carries waitingOnPacket. It is true while the send is queued and the packet whose PDF it attaches is still issuing: an email can now be sent from a packet before the packet has finished issuing, and goes out with the PDF once it has. If the packet’s issuing fails, the delivery becomes failed with a reason that starts “The packet failed to finalize”. An email’s delivery shows whether it arrived. Additive. The status of a delivery on List Record Email Conversations and Get Record Email Conversation, and of each entry in its history, can now be delivered, bounced or complained after sent. A bounce or complaint for any recipient overrides delivered. reason explains a bounced delivery in plain words, as it does a failed one, and sentAt stays set. Calculated values on search results. Additive. searchResultFields rows accept expression, valueType and cardinality together. These use the same expression grammar as list columns, including temporal functions; the Search editor can copy a list column for the same record type. VIEW_AS_OF_DATE() returns today’s UTC date in search. Results and authorized relationship pickers evaluate the expression on read without changing indexed search matches. The add/update search-result-row patch changes accept these properties; updating expression to null restores a field-backed row and clears its type and cardinality. Database-reading functions and attached form numbers are refused. Attached form columns require Quotes or Policies. Configuration import now refuses ATTACHED_FORM_NUMBERS() in list or export columns outside carrier Quotes and Policies lists and entity exports, including broker portal lists and both bordereaux. Object List item rows inherit their parent export’s eligibility and attached forms. Existing Quote and Policy columns and filters are unchanged. See Configure list filters.

November 9, 2026

Import Rows accepts compressed source rows. Additive. Send rowsGzipBase64 instead of rows when a single source record exceeds ordinary request limits. The value is standard padded base64 of gzip-compressed UTF-8 JSON for the complete rows array, limited to 16 MiB compressed and 128 MiB expanded. Authentication and the existing row validation still apply; all values are stored unchanged. See Import Rows. Invoice attachments appear in event files. Additive. File listings, metadata, and placement responses can now include read-only invoice appearances with placementKind: "invoice" and sourceInvoiceId. They require invoice.view alongside file.view, share the attachment’s file and version history, and follow its current linked event. The company chooses a fixed folder path in Company Settings; the default is Billing. Placement writes against these appearances return 409 Conflict; manage the attachment on its invoice. See Invoice attachments on events. Quotes and Policies lists and exports can filter on and show attached forms. Additive. ATTACHED_FORM_NUMBERS() returns the form numbers attached to each row. On Quotes these are the quote’s attached forms; on Policies, the forms in every packet on the policy, the same set its Forms tab lists. In a list filter’s expression it is a String × List value: filter with includesAny, includesAll or excludes through a declared filter. In a list column’s or export column’s expression it returns a sorted list without repeats, so TEXT_JOIN(ATTACHED_FORM_NUMBERS(), ', ') shows them comma-separated. Other lists and exports are refused at import. A configuration that uses it in a list filter on another list, gives it arguments, or uses it anywhere other than a list filter, list column or export column is refused at import. See Configure list filters and the JEXL reference. List columns and list filters can read the Policies list’s “View as of” date. Additive. VIEW_AS_OF_DATE() returns the date the Policies list is viewed as of, as YYYY-MM-DD, in a list column’s expression and in a list filter’s expression. On a list without a “View as of” date, it returns today in UTC, the same as TODAY(). TODAY() is unchanged. A configuration that uses VIEW_AS_OF_DATE() anywhere else is refused at import. See the JEXL reference. A record’s email can be read and its links changed. Additive, for companies with email turned on; for others these endpoints answer 404. List Record Email Conversations and Get Record Email Conversation read the email linked to a claim, insured, submission, quote, person or organization, with the filters the app’s Emails tab has. Link Earlier Record Emails links a person’s email from an earlier date, Unlink Record Person Emails removes the email that came through a person, and Remove Record Email Conversation removes one conversation. List Record Email Link Events shows what was linked or removed, when and why. See the Record Emails overview. A record’s email conversations name the records they mention, and a policy’s email can be read. Additive. Each conversation from List Record Email Conversations and Get Record Email Conversation now carries mentions: the other claims, quotes, submissions and policies its messages name by number, each with its type, id and label. A number that no record carries is left out. For an exposure, person or organization, which have no number of their own, mentionsRecord and the mentionsRecord=true filter now mean the conversation names any record; before, they were always false there. A policy’s email has the same endpoints under the policy API, such as List Policy Email Conversations at /policies/{policyId}/email-conversations, with policy.view to read and policy.endorse to link earlier email. See Records a conversation names.

November 6, 2026

Breaking, for events: an eventOpenCloseHistory sent with a new event must be a well-formed log. A create that sends the log now gets the same check as Replace Event Open/Close History and a PATCH: an open entry, then alternating close and reopen, each no earlier than the one before it by exact time, with every entry’s dates in the years 1900–2200. A log that fails returns 400 InvalidFieldModelV1Data naming each entry it refuses, and nothing is created; an openedOn or closedDate sent with it must still agree with it. Before, the log was stored as sent. A create that sends no log, or an empty one, is unchanged. To load a claim whose source dates contradict each other, fix the log before sending it. See the Event Lifecycle overview. Each invoice type’s approval setting can be read and set. Additive. Transaction categories and policy invoice types now carry moneyOutRequiresApproval, the “Money-out invoices require approval” setting, on List Transaction Categories, List Policy Invoice Types, every Financials settings response and Export Financials Configuration. Creating a type accepts it, optionally: left out, a claims type starts on and a policy invoice type off. Set Transaction Category Money-Out Approval and Set Policy Invoice Type Money-Out Approval change it, and Import Financials Configuration sets it on an existing type when the document states a different value, listing the type in changes.moneyOutRequiresApprovalChanged. A change through the API or an import reaches new invoices only: open invoices keep the approvalRequired they recorded. A change made in the app’s Financials settings can also be applied to the type’s open money-out invoices, which then read the new approvalRequired. See Approval and payments. Invoices say whether they require approval, and money coming in no longer does. Additive, with a behavior change for payments. Every invoice now carries approvalRequired beside approved, on every read and write response. It is recorded when the invoice is created: true for money going out (a positive totalAmountCents) on an invoice type whose “Money-out invoices require approval” setting is on, which is every claims type and no policy invoice type to start with, and false otherwise, including all money coming in. A zero total follows its type’s expected direction. Record Payment on an invoice with approvalRequired: false now goes through for a key without invoice.approve, where it returned 422 NOT_APPROVED before, and no longer approves the invoice when the key holds the permission. Invoices that require approval behave as before. Invoices created before today carry a value recorded by the same rule, as if every claims type’s setting had always been on and every policy invoice type’s off. Approvals already given are kept. See Approval and payments.

November 5, 2026

Create a linked set of records in one request. Additive. Create Entity Batch saves exposures, submissions, and new-business or renewal quotes together, every one or none. Each entry names itself with ref, and a later entry links to it by writing "ref:<name>" where a record id goes: a relationship field, or an embedded exposure’s id. A save needs batchCreateOptions.idempotencyKey, and a retry with the same key and body returns the same ids with created: false. ?dryRun=true runs the same checks and saves nothing. A refused entry answers with the error Create Entity would return, plus entity naming it, and nothing is saved. See Batch create.

November 4, 2026

Policy financials can be managed through the API. Additive. Callers with financials.configure can list policy invoice types and line items with IDs (including deprecated identities), create, rename, delete, deprecate and reactivate them, set invoice-type direction, payee defaults and presets, and change allowed associations and display labels. The operations use the same rules as Financials settings: non-deleted invoice citations block rename/delete, and legacy items must be deleted explicitly before their original invoice type. Import remains additive. See Policy financials configuration. An Auto-Add-Forms header on a create other than a quote’s says to remove it. Messages only; codes are unchanged. Create Entity refuses every old option header with 400 ENTITY_OPTION_MOVED. On an entity type other than a quote, the message for Auto-Add-Forms named entityCreateOptions.autoAddForms, which that type refuses. It now says to remove the header, which never did anything there. See Request bodies. Option entries in a field schema name their group. Additive. In Get Entity Configuration, each oneOf entry of an option-set field now carries x-group, the option’s group label, when the option has one. Options without a group are unchanged. A set that repeats a label across groups, such as an injury list with a “Burn” under each profession, can now be told apart from the schema alone, as the app’s grouped dropdown does. The MCP describe_api tool shows it as group. See Data models. Breaking: entity writes need entityData. Create Entity and Update Entity now refuse the flat body, with the record’s fields at the top level, with 400 ENTITY_DATA_REQUIRED, whose message names the shape to send: { "entityData": { … }, "entityCreateOptions": { … } } on a create, { "entityData": { … }, "entityUpdateOptions": { … } } on an update. Nothing is written. An empty body is refused the same way: send { "entityData": {} }. Before, the flat body and its option headers still worked, though deprecated. Move the fields into entityData and the options into entityCreateOptions or entityUpdateOptions: every option keeps its values and meaning, and a create keyed in the flat body still replays when it is retried with the same key in the new shape. See Request bodies. An update names the read-only fields it did not keep. Additive. Update Entity now answers { "id": …, "ignoredFields": [ … ] } when the request sent a value for a readOnly field, such as a quote’s fullTermPricingInfo, that the update did not keep: a platform-owned value is dropped and a calculated one is recalculated, as before, and the update still returns 200. ignoredFields is absent when there are none, including when a read-only field only repeated the value it already holds, as a read-modify-write does. Nothing else changes. See Update Entity. Entity writes take their fields and options in separate objects. Additive. Create Entity takes { "entityData": { … }, "entityCreateOptions": { … } }: the record’s fields in entityData, keyed by field referenceId as before, and its options in entityCreateOptions, which is optional. The create’s option headers become options: Idempotency-Key is idempotencyKey (a UUID), Idempotency-Owner is idempotencyOwner (it requires the key), and Auto-Add-Forms is autoAddForms, the JSON boolean true or false, for quotes only. A quote’s top-level pricingSetup is entityCreateOptions.pricingSetup. Update Entity takes { "entityData": { … }, "entityUpdateOptions": { … } }: only the fields in entityData change, and Complete’s invoices: "generate" is entityUpdateOptions.invoices. expectedEntityHash, expectedConfigurationHash and expectedInvoicingEnabled stay query parameters. Every option keeps its values and meaning, and a keyed create retried in either shape replays the original. A body in the new shape returns 400 ENTITY_DATA_REQUIRED, naming the shape, when it sends options without entityData; 400 ENTITY_OPTION_MOVED, naming the option to use, when it comes with an old option header, or puts pricingSetup in a quote create’s entityData or invoices in a quote update’s; and 400 INVALID_BODY for any other key, an unknown option, or a quote option on another entity type. Reads are unchanged and still return fieldModelV1Data. Every other endpoint keeps its body, and those that take Idempotency-Key, such as New Business, Endorse and Bind Quote by Reference or by Value, keep the header. The flat body, with fields at the top level and options in headers, still works but is deprecated: a later change will refuse it, with its own Breaking entry here. See Request bodies.

November 3, 2026

A Date written from today names its timezone. A behavior change for Validate Configuration, Import Configuration and Patch Configuration. The formula function TODAY now takes an optional timezone: TODAY('America/Los_Angeles') is today’s date there, and TODAY() is still today’s date in UTC. A configuration whose Date field is calculated from TODAY() without a timezone, or whose auto-set writes a Date from it, is now refused with 400 and the finding DATE_TODAY_ZONE_REQUIRED: write DATE_VALUE(TODAY(policyTimeZone), policyTimeZone). TODAY() keeps working everywhere else. Every company’s configuration has moved. See the JEXL reference. Claims created in the app open in the creator’s timezone. Additive. The app now sends openedOn as today in the creator’s timezone, so a claim created in the app at 9 PM in Los Angeles opens on that day in America/Los_Angeles, timed at its creation; it used to open on the next day in America/New_York. Its openedOn and first eventOpenCloseHistory entry carry that timezone. A create that sends no openedOn still opens on today in America/New_York. A create that sends only openedOn, dated today in its own timezone, no longer needs the field write group of eventOpenCloseHistory: it is the open the platform adds anyway. Any other openedOn, a closedDate or a sent history still needs it. See Why the status has its own endpoints. For events: openedOn and reopenedOn follow exact times. A behavior change for an event’s openedOn, reopenedOn and previousClosedOn, and for the PREVIOUS_CLOSE formula function. openedOn is now the date of the open entry with the earliest effectiveAt, and reopenedOn the date of the reopen entry with the latest, the one listed last on a tie; they were picked by date before, ignoring its timezone. A re-open at 10 PM in Los Angeles that came after one at 12:30 AM the next day in New York is now the claim’s reopenedOn, and previousClosedOn is the close listed just before it. All three stay dates. Values change only where an entry’s date and its exact time disagree across timezones; an event stores the new values the next time it is written. See The exact time. The claim history’s order is checked by exact time. A behavior change for Close Event, Re-open Event, Replace Event Open/Close History, closedDate on an event create or update, and eventOpenCloseHistory on an event update. Each new entry is compared with the one before it by exact moment, not by day: a tie is allowed, and an entry whose time nobody gave (effectiveTimeKnown: false) counts as the start of its day. A close at 10 PM in Los Angeles can now follow an open at 12:30 AM the next day in New York, which was refused before, and a close at 1 PM is now refused after an open at 2 PM the same day, which was accepted before. A date sent without effectiveAt is refused only when the entry before it has a real time later that same day; the message says so and gives the earliest time allowed, so send effectiveAt. These refusals keep their status and code (409 EventLifecycleDateOutOfOrder, or 400 for a log sent in full), and their messages now name times and timezones. See The exact time. A Closed Date edit can carry the time of the close. Additive. On a closed event, a PATCH that changes closedDate may add effectiveAt inside it: the exact moment of the close it moves, a timestamp with an offset or Z on the new date in its timezone, such as { "date": "2026-09-12", "timezone": "America/New_York", "effectiveAt": "2026-09-12T15:20:00-04:00" }. The event’s last close takes that time, with effectiveTimeKnown: true; without it the close is timed as before. The same date with another effectiveAt re-times the close. closedDate itself stays a plain date, and reads never return the time. A time on another day or without an offset returns 400, and so does one sent with eventOpenCloseHistory or on create. See Editing the Closed Date.

November 2, 2026

Older claim history entries have a placeholder time. Additive. Every eventOpenCloseHistory entry written before exact times existed now has an effectiveAt, with effectiveTimeKnown: false: the start of its effectiveOnDate in that date’s timezone, or one second after the entry listed before it when that one falls on the same day, and never later than an entry with a time listed after it on that day. Entries that already had a time keep it. See The exact time. Breaking: a bind by quote says whether it adds the quote’s policy forms. Bind Quote, and Bind Quote by Reference or by Value with a quoteId, now require bindForms in the body, for every quote type, so Bind Quote’s body is no longer optional. bindForms: true adds the quote’s recommended policy forms if that kind was never added, by the rule the bind has followed since October 27: a form someone removed stays removed, a quote created with Auto-Add-Forms: false gets none, and the forms are drafted from the quote as it stands at bind and filled with the bound policy’s values. bindForms: false binds with the forms the quote holds and adds none. Leaving it out returns 400 BIND_FORMS_REQUIRED, whose message names both choices; before, the bind added the forms without being asked. A bind that asks for the forms and can’t add them is now refused, and nothing is bound: 403 BIND_FORMS_FORBIDDEN when the key lacks form.create or form.edit, even when there are none to add, and 500 POLICY_FORMS_NOT_ADDED when evaluating the rules, drafting or linking the forms fails; retry it, or send false. Before, the bind skipped the forms in both cases and bound the quote. A quote changed while its forms are being added is still refused with 409. Bind Quote now answers 201 { policyId, addedForms }, where addedForms lists each form the bind added as { id, formNumber, name }, in rank order, or is []; a bind by reference answers the full policy-version result plus addedForms, and a retry with the same Idempotency-Key returns the same list. A bind by value is unchanged and refuses bindForms as an unknown key with 400. Check Bind Conditions and quote creation are unchanged. Send bindForms: true to keep adding the forms, from a key that holds form.create and form.edit, or false to bind without them. See Recommended forms on quote create. Breaking: the built-in financial export columns, smart tags and Reporting frames are removed. Configured export columns and smart tags that read the eventFinancials, exposureRelatedEvents, policyRelatedEvents and policyInvoices roots replace them, and every company’s presets, templates and configured tables already use those (ADR 0099). What changes for an API caller:
  • Import Financials Configuration refuses a document whose reporting.exportSurfaces or reporting.invoiceTypeOverrides names the eventExport or smartTags surface with 400, and nothing is written. The reporting surfaces are transactions, overview and reserveUpdates, and Export Financials Configuration names only those, so re-export before you round-trip a document saved earlier.
  • Start Entity Export Run refuses a fields[] entry of type financial with 400, and export presets refuse a finv2::overview::… field key with 400 UnknownExportFieldKey. The finv2::overview::… keys are no longer built-in columns of the event surface, so a configuration import refuses an exportPlatformColumns entry naming one. An event export’s asOf and startDate set the Financial Reporting Period that configured eventFinancials, eventReserveUpdates and eventInvoices columns read; an export that sends neither reads from the beginning of time to today, as before.
  • The built-in Claim/Incident financial tags (AIIFmv1Fin…), the Insured loss-run tags (Claims Details Table, Claims Total Paid, Claims Total Reserve and Claims Total Incurred) and the Policy Policy Invoice Line Items tag no longer exist. Uploading or replacing a template that contains one is refused with 400 invalid-smart-tags, naming each tag. The Insured Policy History Table tag stays.
  • exposureRelatedEvents rows on Insured forms no longer carry paid, reserves, incurred, their …Formatted text, or the per-type finv2::overview::… keys; they carry the keys a policyRelatedEvents row carries. A declared SMART_TAG_TABLE column that names one of them is a configuration import error, and an expression that reads one finds nothing and prints blank. Read each row’s figures through its eventFinancials.
Claim history entries carry an exact time. Additive. Each entry of an event’s eventOpenCloseHistory now has effectiveAt, the exact moment the action takes effect, as a UTC timestamp to the second (2026-11-02T19:05:33Z), and effectiveTimeKnown. Close Event and Re-open Event take an optional effectiveAt (a timestamp with an offset or Z, on the day of effectiveOnDate in its timezone), and so does each entry sent to Replace Event Open/Close History, on create or in a PATCH. An entry written without one takes the current time when its effectiveOnDate is today in its timezone, otherwise the start of that day with effectiveTimeKnown: false. A timestamp without an offset, or on another day than its effectiveOnDate, returns 400. Closed Date, Opened On and the other claim dates stay plain dates, and the order rules still compare days. Entries written before this change have neither value. See The exact time. New: List Company Members. Additive. List Company Members (GET /api/v1/companies/{companyId}/members) returns every member of the company with their id, name, email and role, and requires member.view. A member’s id is the user ID that task assignees and export-preset ownerUserId accept, so an API client or the MCP agent can now assign a task to a colleague or act on their export presets without knowing their ID in advance. Calculated fields name what they are calculated from. Additive. In Get Entity Configuration, a calculated field now carries x-derived-from: the referenceIds of the same entity’s fields its value is calculated from, which are the fields to set instead. A submission’s or quote’s primaryInsured lists exposures, so you set the primary insured with an exposures item whose policyRelationshipType is primary. A primaryInsured value sent directly is dropped. The MCP describe_api tool shows the mark as derivedFrom. The entity docs now state the create contract per field: a field in required must be sent, a field that is not readOnly may be sent, and a value sent for a readOnly field is dropped without an error or a warning. Write behavior is unchanged. See Field write tiers.

October 31, 2026

Sandbox file migrations. Additive. Five POST endpoints under /api/v1/companies/{companyId}/files/migrations/—prepare, state, refresh, finalize and reconcile—import ordinary files using durable source identities, resumable uploads and generation-pinned size/checksum verification. All require file.view, file.upload and file.edit; reconcile also requires file.configure-categories. Reconciliation requires the source revision and version ID, preserves categories and folder ancestry, and reports conflicts with operator edits instead of overwriting them. Retries retain the same logical file; historical imports require a verified source-provenance backfill before adoption into this workflow. See Sandbox file migrations. Whole-policy imports. Queue Import Job accepts entityType: "Policy" with policyMode: "whole-policy" or "single-transaction", guarded by policy.import. The contracts describe term values, dated full-state segments, renewal links, cancellation details, transaction receipts and policy dry runs. Whole-policy jobs write all segments and an optional final cancellation atomically, or validate the sequence without policy writes when dryRun: true. Single-transaction jobs now execute NEW_BUSINESS, RENEW, ENDORSE, CANCEL and REINSTATE with one atomic receipt, or a read-only validation result. Policy field write groups and the invoicing-off stated-billing refusal apply to both import modes, including implicit removals during whole-policy replacements. Existing entity jobs and synchronous imports are unchanged. See Policy import contracts. Insured bordereau exports. Bordereau export runs accept surface: "insured-bordereau" to report changed insureds from each policy transaction’s embedded snapshots. The corresponding configuration surface declares Exposure columns and policy expressions through transaction.after. Files always include transaction and insured identities. Existing requests keep their policy-transaction rows and fixed-column defaults.

October 30, 2026

Staff only: Retire Quote Rating is removed. POST /api/v1/companies/{companyId}/quotes/{quoteId}/rating/retire no longer exists. It retired ratings while each company moved to calculated pricing, and every company has moved. A quote with no current rating for a workflow still reads Not Checked, and Clear Quote Rating still removes a rating and everything its workflow owns. Import transformations support strict conversions and nested lists. The JEXL reference now includes text and number parsing, integer scaling, StringOrNumber construction, explicit-zone calendar dates, entered addresses, phone parsing with retained warnings, and scalar option crosswalks. LOOKUP_VALUES resolves dynamic lists with LOOKUP’s permissions and ambiguity rules. A mapOver value may contain another mapping, and { values: { ... } } constructs one object in the surrounding scope; me and parent follow the nested list while root remains the original data. Existing expressions and flat mappings keep their behavior. A configuration’s Time Zone set must include the US territories. Validate Configuration, Import Configuration and Patch Configuration now refuse a configuration whose Time Zone option set lacks America/Puerto_Rico, America/St_Thomas, Pacific/Guam, Pacific/Pago_Pago and Pacific/Saipan, as announced on October 29, 2026; until now they still accepted the previous list without them. Validate reports an error naming the missing zones; import (forced or not) and patch return 400 config-validation-failed, and nothing is saved. Every company’s configuration lists them today, so no configuration needs to change. Export money cells round to the cent, and FORMAT_CURRENCY never prints -$0.00. A number export column in Currency or Accounting format now rounds each value to the cent before it writes the cell, both the number the Excel file and Google Sheets spreadsheet hold and the text the CSV prints. A Paid column computed as end minus start shows $50.05, not 50.04999999999999, and a value that rounds to zero writes 0, shown as $0.00, or $ - under Accounting. Other number formats keep the value’s own digits. FORMAT_CURRENCY prints a negative zero, or a negative amount that rounds to zero at the decimals shown, as $0.00 (0.00 with symbol: false), where it printed -$0.00. A real negative amount still prints -$5.00. A Closed Date in another time zone is an edit, and dates entered in the app keep their zone. A behavior change for closedDate on an event update. On a closed event, a closedDate with the day the event already holds but another timezone now moves the last close in eventOpenCloseHistory to that timezone, through a generic PATCH or the MCP tools, as a new day does; before, the same day in any timezone was not an edit, and nothing was written. A closedDate sent without a timezone is in America/New_York, as on every Date, so on a close stored in another timezone it is now an edit too. In the app, people can choose a date’s zone, and a new day keeps the zone the date already has; before, every date entered in the app took the zone of the person’s browser. A new date on a quote, policy or submission takes its policyTimeZone, and on other records the browser’s zone. The Date wire format is unchanged. See Editing the Closed Date. Cards can start expanded or collapsed. Additive. A card configuration accepts collapse: "expanded" | "collapsed" to add a collapse control with that initial state. Collapsing leaves its header visible and preserves field values, required answers and validation. Omit collapse to keep the existing always-open behavior. The setting survives configuration import and export, card detail and page-layout reads. The add-card and update-card typed changes accept it; update-card with collapse: null removes the control. Delete Entity takes the same optional hash guards as Update Entity. Additive. Delete Entity accepts the optional expectedEntityHash and expectedConfigurationHash query parameters that Update Entity takes, with the same values: the SHA256 of the complete external GET envelope you reviewed, and configuration metadata’s contentHash. Both are checked under the entity’s row lock, in the delete’s transaction. A stale entity hash returns 409 EntityPreimageChanged, a stale configuration hash 409 EntityConfigurationChanged, and nothing is deleted. A value that is not 64 lowercase hex characters is 400 InvalidEntityPreimageHash or 400 InvalidConfigurationPreimageHash. A delete that sends neither is unchanged. Additive: Broker Portal decline forms in configuration. The brokerPortal section accepts an optional declineForms list of form numbers: the Submission form templates the decline dialog attaches to the copy of the application by default, in attachment order. Validate Configuration and Import Configuration refuse a number that names no live template of the company, a template whose category is not submission, and a repeated number. Patch Configuration accepts a new change kind, set-broker-portal-decline-forms, which replaces the list or clears it with an empty list. Compare reports the list under brokerPortalDeclineForms, one record per form number. Export includes it only when it is set. See Broker Portal decline forms. Import storage and transformation are separate operations. Import Rows stores arbitrary JSON values without mapping. Queue Import Job maps stored rows in the background, selected by IDs, batch or JSON filter, optionally grouped into one result. Choose an entity type per job and queue another job to rerun a mapping. Get Import Job reports progress; list attempts with jobId for result details and source-row provenance. Storage-only rows have entityType: null and are visible to any caller with an import permission in the company. Existing synchronous import/retry endpoints remain available.

October 29, 2026

New: the submission form category. Additive. A form template can be uploaded, listed and generated under the submission category, and a smart tag takes the submission form type. A submission tag reads the Submission the form is generated against as submission.<field>, with the Submission’s field definitions; submission.exposures, the embedded exposure list, prints as a table through SMART_TAG_TABLE. A submission form offers no other root and no built-in tags (current date, generation stamps), and keeps no as-of date, so $form is refused on it. submission is refused, as an import error, in a smart tag of any other form type. The category and formType enums on Upload Form Template, Upload Static Form, List Forms, List Generated Forms and the smart-tag configuration changes list submission, and now list insured, which they already accepted. Existing form types behave as before. See the JEXL reference. Dates on screen show their time zone, and a Date placement can hide it. Additive. Every screen that shows or enters a Date now shows the zone the value is stored with after the day, such as 10/27/2026 America/New_York. A Date placement’s inputModalityArgs or displayFormatArgs takes hideTimeZone: true to hide it, for a day where a zone means nothing, such as a date of birth; a list column or a global search result hides it too when every placement of its field does. Configuration import, export and validation carry the key like any other placement arg. Stored values and the Date wire format are unchanged, and exports and generated documents still print the day alone. The Time Zone options include the US territories. Additive. A quote’s or policy’s policyTimeZone, and the configuration’s graphSettings.timeZone, now accept America/Puerto_Rico, America/St_Thomas, Pacific/Guam, Pacific/Pago_Pago and Pacific/Saipan; before, they were refused with 400. Each company’s Time Zone option set lists them among the other zones in alphabetical order, each labelled with its own name. TIMEZONE() now answers them for an address in Puerto Rico, the US Virgin Islands, Guam, American Samoa or the Northern Mariana Islands, where it answered null, so a quote’s Time Zone fills from such an address too. An American Samoa address with map coordinates still answers null, because the coordinates resolve to independent Samoa; without coordinates it answers Pacific/Pago_Pago. A configuration import accepts the Time Zone set with or without the five for now; a later change will require them. Dates accept any IANA time zone on every field. Additive. A Date’s timezone may be any zone in the IANA time zone database on every write path. Before, the platform’s own Date fields (an event’s eventDate, reportDate, openedOn and closedDate, for example), Close Event, Re-open Event and Replace Event Open/Close History refused a zone outside the United States with 400; your own Date fields already took any zone. An older name for a zone is now accepted, and stored and returned under its current name (Asia/Calcutta as Asia/Kolkata); before, it was refused with 400. A date entered in the app carries the zone of the person’s browser, where a zone outside the United States used to become America/New_York, so responses can now carry zones outside the United States. A missing zone still defaults to America/New_York. See Date. New: rename a form by its number. Additive. Rename Form, PATCH /api/v1/companies/{companyId}/forms/{number} with { "name": "…" }, changes the display name of a DOCX template or a static PDF form and returns the same summary List Forms returns. Only the name changes: the number, templateKey, category, version and file stay the same, so forms-logic rules keep resolving. The name is trimmed and must not be empty (400 form-template-name-empty); an unknown number is 404 form-template-not-found. Requires form-template.edit, the permission the replace endpoints use. New: set a field’s Broker Portal kick-out question one field at a time; clearanceMatch there now needs the Broker Portal on. Additive for kickOut, and a new refusal for clearanceMatch. The Patch Configuration add-field verb takes an optional kickOut, { appliesWhen?, acceptWhen, reason }, the same object a configuration import carries, and update-field sets it or clears it with null, so a kick-out rule no longer needs a full configuration import. The rules are the import’s: a top-level Submission or Exposure field that is not systemManaged, a non-empty reason, and appliesWhen and acceptWhen boolean JEXL over the record’s fields and value; a custom-object sub-field is refused on both verbs. Setting kickOut or clearanceMatch through either verb is now refused with 400 broker-portal-off-for-portal-field-declaration unless the company’s Broker Portal is on, and nothing is written. Before, clearanceMatch was accepted whatever the Broker Portal’s state. Clearing either with null is always allowed, and configuration import is not gated, so a carrier can stage Broker Portal configuration before turning the portal on.

October 28, 2026

New: graph data points and graph settings in configuration. Additive. A configuration import, export and validation body takes two new optional sections. graphSettings is { timeZone }, one of the Time Zone vocabulary’s zones: the zone graphs read today and the day each timestamp falls on in, shared by every user. graphDataPoints rows are { key, label, source, value, date?, group?, rowSource? }, one per company-unique key, a camelCase identifier. source says what one row stands for: a record of one of the six record types (event, exposure, quote, submission, person, organization), a policy (policy, read from the most recent segment of its latest version), a segment of a policy’s latest version (policy-segment) or a policy transaction (bordereau). value, date and group are each a bare field referenceId or a pure JEXL expression over the row: value a number, date a day, group a scalar a graph can split by. Every source offers record.createdAt, the day the record was created in the graph time zone; policy-segment adds segment.startDate and segment.endDate, and bordereau adds transaction.transactionType, effectiveDate, endDate, transactionTimestamp and policyVersion. rowSource names a saved Object List field of a record source’s type, one row per item. The Patch Configuration takes add-graph-data-point, update-graph-data-point (null clears date, group or rowSource), delete-graph-data-point and set-graph-settings. Configuration import refuses, as errors: data points without graphSettings; a duplicate key; a bare component that names no field of its row, or a field of a kind it cannot read (a List field, a non-numeric value, a non-Date date, an Object, Address or other non-scalar group); a rowSource that is not a saved Object List field, or on a policy source; a field named record on a record type a data point reads, or segment on Policy for policy-segment; and a JEXL component that does not compile, calls anything but pure functions such as TODAY(), or reads a root its source does not offer, financial roots included. Dashboards do not show data points yet. New: a field’s clearanceMatch marks it as identifying an insured. Additive. A field configuration row takes an optional clearanceMatch, one of name, taxId or address, carried by configuration import, export and validation, describe/fields, and the Patch Configuration add-field and update-field verbs; update-field clears it with null. It names how the duplicate-submission check compares the field’s value: name ignores case, punctuation and trailing legal suffixes such as Inc and LLC, taxId compares digits only, and address compares the entered street, city, state and 5-digit ZIP. Configuration import refuses it, as an error, on a field that is not a top-level Exposure field, on a List field, and on a field whose kind does not fit the value: name and taxId need a Text field, address an Address or AddressV2 field. A field without it takes no part in the check. The check flags another submission whose primary insured shares a value on any marked field; a configuration that marks none compares exposureName as a name. New: each eventInvoices and policyInvoices line carries its displayLabel. Additive for Claim/Incident and Policy smart tags and event and policy export columns. A line of an invoice row, and the record a SMART_TAG_TABLE with lineItems: true lists, now carries displayLabel beside lineItemType: the label the invoice’s type gives that line item type in Policy Financials, else the line item type’s name. Only Policy invoice types give labels, so an event line’s displayLabel is its lineItemType. A line items table whose Line Item column reads from: "displayLabel" prints what the built-in Policy Invoice Line Items tag prints, so a Broker Commission line on an Agency Bill prints Broker Commission (Less). lineItemType is unchanged. See Form templates. New: a card’s section lists it in the Broker Portal submission page’s jump-to section nav. Additive. A card takes an optional section, a non-empty label, carried by configuration import, export and validation, card descriptions and page layouts. Patch Configuration add-card and update-card set it, and update-card clears it with null. A card without one is not listed. See Card sections. New: SMART_TAG_TABLE lists an invoice table’s line items with lineItems: true; TYPE_SIGN signs one invoice line. Additive for Claim/Incident and Policy smart tags. SMART_TAG_TABLE’s layout takes lineItems: true over eventInvoices or policyInvoices rows: the table lists the invoices’ line items instead of the invoices, one row per line, in invoice order then line order, each { lineItemType, direction, amount, paid, invoice }, where invoice is the line’s invoice row without its lineItems. Columns, groupBy and subtotals read that record, so from: "invoice.transactionId", row.invoice.invoiceType, groupBy: "invoice.transactionId" and a subtotal over each invoice’s lines work, and it may stand beside print: "paragraphs". An invoice with no lines lists nothing. Such a table prints what the built-in Policy Invoice Line Items tag prints, as of the form’s date; see Form templates. TYPE_SIGN now also reads one line of one invoice row, its amount or paid, signed by the line’s own direction, which can differ from its invoice’s: a lineItems: true table’s row, row.lineItems[0] in a table of invoice rows, or FIRST(FIRST(eventInvoices.end.eventInvoicesRows).lineItems). Configuration import refuses, as errors: a lineItems that is not true or false; lineItems: true over any rows but eventInvoices or policyInvoices rows, filtered, sorted or dated, BY_PAYEE’s rows, a PICK, reserve-update, summary and related rows and field paths included; a column, row path, group key or subtotal path the line’s record does not carry, such as transactionId where the invoice’s is invoice.transactionId; and TYPE_SIGN over any other line, such as one of eventFinancials’ lineItems, or over a line’s figure other than amount or paid. The built-in Policy Invoice Line Items tag is unchanged. See the JEXL reference. An endorsement quote that changes an exposure, or another item in a policy list, that the policy drops before the endorsement ends now rates and binds. Rating such a quote, Check Bind Conditions, Bind Quote and Bind Quote by Reference or by Value used to return 400 InvalidDelta (“No element in ‘exposures’ matches id = …”): for example, an endorsement effective December 1 that changes the hours of an exposure that an endorsement already on the policy removes from February 10. The change to that item now ends the day before the item leaves the policy, and the saved delta records it: its endDate, as Get Policy Transaction returns it, is that day rather than the endorsement’s end. The endorsement’s other changes keep their dates. Endorse Policy Transaction is unchanged: a delta you send that writes to an item on a day the policy does not hold it is still refused with 400 InvalidDelta.

October 27, 2026

New: BALANCE_DUE_BY_AGE and BY_PAYEE over invoice rows; DATED_BETWEEN and TYPE_SIGN read invoice rows. Additive for Claim/Incident and Policy smart tags and event and policy export columns. BALANCE_DUE_BY_AGE(rows, invoiceTypes, fromDays, toDays, options?) sums the balanceDue of the eventInvoices or policyInvoices rows of the chosen invoice types whose age is from fromDays through toDays days, both included: the days past due, or the days since invoiced with { ageFrom: "invoiceDate" }. toDays may be null for no upper bound, an invoice that lacks the date is in no range, and { undated: true } with both bounds null totals those instead, so one column per bucket makes an aging report, for example BALANCE_DUE_BY_AGE(eventInvoices.end.eventInvoicesRows, ["Indemnity"], 91, null, { sign: "type" }) for the Indemnity balance more than 90 days past due, positive. BY_PAYEE(rows, invoiceTypes, options?) groups the same invoices by payee, one row per payee ordered by name, { payee, invoiceCount, amount, paid, balanceDue }, the invoices with no payee last with a null payee; its rows feed SMART_TAG_TABLE, a filter, COUNT or FIRST, for example FIRST(BY_PAYEE(policyInvoices.end.policyInvoicesRows, ["Premium"])).balanceDue. Both read invoice types as FINANCIAL_CATEGORY_TOTAL does: names or a selector, sign: "type", and cents rounded per value. Both are null over null rows; a name or a selector that matches none of the invoices adds nothing, so they are 0 and [], as over no invoices. DATED_BETWEEN now also keeps invoice rows, by invoiceDate, never one with no invoice date, for example COUNT(DATED_BETWEEN(eventInvoices.end.eventInvoicesRows, eventInvoices.start.date, eventInvoices.end.date)), and TYPE_SIGN reads one invoice row’s amount, paid or balanceDue. Configuration import refuses, as errors: either new function over any rows but eventInvoices or policyInvoices rows, filtered, sorted or dated, or in a table column or subtotal; invoice types and options that are not literals of their shapes; day bounds that are not literal whole numbers, 0 or more, with fromDays no more than toDays; undated with a day bound, or a null fromDays without it; a name that is not an Event invoice type over eventInvoices rows or a Policy invoice type over policyInvoices rows; a selector that matches no type; and sign: "type" with {} or with payable and receivable types together. See the JEXL reference. eventInvoices and policyInvoices in Claim/Incident and Policy smart tags and in event and policy export columns; import refuses them elsewhere. Additive for these four surfaces: a Claim/Incident smart tag and an event export column can read eventInvoices, the Event’s Invoices table, and a Policy smart tag and a policy export column can read policyInvoices, the policy’s Invoices table, each invoice as it stood at the start and end of a reporting period. eventInvoices.end.eventInvoicesRows lists the Event’s invoices at the period’s end, newest recorded first, each with transactionId, invoiceNumber, invoiceType, expectedDirection, invoiceDate, dueDate, status ("no_charges", "owed", "partially_paid", "paid" or "voided" as of that date), payee (id, name and kind: "person", "organization" or "exposure"; null without one), amount, paid, balanceDue, daysPastDue, daysSinceInvoiced, lastPaymentDate, memo, payments (date, amount, memo) and lineItems (lineItemType, direction, amount, paid); a policyInvoices row also carries scheduledDate, and an installment scheduled after the date reads "scheduled". An invoice is listed once its invoice date, its recording or a payment dated by then falls on or before the date; drafts and deleted invoices never are. paid counts the payments dated by the date and balanceDue is what was still due on it, so for each invoice type the rows add up to eventFinancials’ or policyFinancials’ paid and owed at the same date. Money is in dollars and the plain sign; a voided invoice reads 0. On the event export the period ends on asOf, else today, and starts on startDate; without a startDate, start.date is null and the start rows are []. On the policy export and on forms it ends on the as-of date, else today, and starts at the beginning of time. An Event reported after the period’s end keeps its export row and its form, with null rows at both ends, and a Policy form’s policyInvoices is null before bind. Configuration import refuses, as errors, either root in any other smart tag or export column (Insured and Quote forms, the exposure, bordereau, quote, submission, person and organization exports, and item rows included), an Event field named eventInvoices, a Policy field named policyInvoices, and either root’s rows as the source of FINANCIAL_CATEGORY_TOTAL or EARNINGS_TOTAL. See the JEXL reference. New: download a quote’s or policy’s form packets. Additive. List Form Packets, GET /api/v1/companies/{companyId}/forms/packets?entityType=quote|policy&entityId=…, lists the packets issued on a quote or policy, newest first, each with its status and its documents in page order. Get Form Packet Download URL, GET /api/v1/companies/{companyId}/forms/packets/{packetId}/download-url, returns a 15-minute signed URL to an issued packet’s merged PDF, the same file the app’s Forms tab downloads. A packet still being issued returns 409 FormPacketNotIssued. Financials configuration import takes a content-hash precondition: expectedContentHash. Additive. Export Financials Configuration returns an X-Content-Hash header, the sha256 of the exported document with keys sorted at every depth. Import Financials Configuration takes ?expectedContentHash= with that value and imports only while the configuration still exports with it, checked inside the import’s transaction and under ?dryRun=true too. A mismatch is 409 financials-config-expected-hash-mismatch with currentContentHash in the body, and nothing is written. A value that is not 64 lowercase hex characters is 400 financials-config-expected-hash-malformed. An import without it is unchanged. A quote created through the API gets its recommended policy forms when it’s bound. POST /api/v1/companies/{companyId}/entities/quote now attaches only the quote’s recommended quote forms. Its policy forms are added when the quote is bound with Bind Quote, or Bind Quote by Reference or by Value with a quoteId, from the quote as it stands then, so a PATCH or a rating after the create counts, and they’re in the bind packet with the bound policy’s values. The bind now drafts them, so it takes longer by about the time the create used to spend on them, and the create is that much faster. A bind adds them only if they were never added: a form someone removed stays removed, a quote created with Auto-Add-Forms: false gets none, and a quote built in the app and bound through the API gets the ones it never had. The binding key needs form.create and form.edit for this, as the creating key does for quote forms. A bind by value adds no forms, as before. If the forms can’t be added, the quote is still bound. A quote changed while its own bind is adding them is refused with 409; bind it again. Before, the policy forms were attached when the quote was created and never checked again. See Recommended forms on quote create. Breaking: only Cancel and Reinstate switch a policy off and on. policyStatus and cancellationEffectiveOnDate are owned by the platform, and a request that would set them anywhere else is now refused with 400 SystemOwnedPolicyValue, and nothing is saved. That is a New Business Transaction or Renew Policy Transaction whose data states a policyStatus other than "active" or a cancellationEffectiveOnDate other than null, and an Endorse Policy Transaction delta that would change either one, directly or inside a whole-policy Overwrite. The message names the field and says to use Cancel Policy Transaction or Reinstate Policy Transaction. Restating the value the policy holds still works, so a whole policy object read and sent back is accepted: "active" and a null date on new business and renewal, the policy’s own values on an endorsement. policyStatus is no longer required on new business and renewal; a new policy starts active. Get Policy Configuration marks both fields read-only. Before, these requests stored the value, so a policy could look cancelled without ever being cancelled: never priced, billed or refunded as a cancellation, refused every endorsement, and unable to be reinstated. To load a cancelled policy, create it and then cancel it. See Cancellation and Reinstatement. Breaking: search is declared in searchFields and searchResultFields, not on fields. A configuration import, export and validation body takes two new optional sections of { surface, key, label? } rows, one row per (surface, key), where surface is one of the seven record types. A searchFields row names a value listing search and global search find a record by: key is a field’s referenceId on that record type, or a dotted path through custom-object fields to a sub-field, such as vehicles.vin, so a sub-field is searchable only on the record types and paths a row names. A searchResultFields row names a top-level field a global search result shows. label names the field on a result. The Patch Configuration takes add-search-field-row, update-search-field-row and delete-search-field-row, and the same three for search-result-field-row; update- sets label or clears it with null, and delete-field now also removes the deleted field’s rows, paths under it included. Import refuses a row whose key names no field on its record type, a searchFields row of a Boolean, Object, Join, Pointer or EmbeddedExposure value, and a searchResultFields key that is a path. It refuses the field-level searchable and showInSearchResults added on October 24, which fields, custom-object sub-fields, describe/fields rows, add-field and update-field no longer carry. A record’s display name is still searchable without a row; a Submission’s submissionName and a Person’s or Organization’s email are now searchable only with one. No configuration declared the field-level properties. Staff only: Retire Quote Rating. Additive. Retire Quote Rating, POST /api/v1/companies/{companyId}/quotes/{quoteId}/rating/retire with { "ratingWorkflowName": … }, retires a saved quote’s current rating for that workflow, whatever its freshness. Nothing is saved to the quote, which keeps its rating’s results and price; the workflow reads Not Checked, so Bind Quote and Check Bind Conditions raise nothing for it until the quote is rated and saved again. A quote with no current rating answers noCurrentRating. A bound or cancelled quote returns 409. It needs a staff Bearer token or a verified platform service identity; API keys are rejected. Moving a company to calculated pricing uses it on every rating of every open quote, so taking the pricing stage out of a workflow leaves no Rating setup changed warning on the quotes rated before the move. Cancellation and reinstatement quotes carry the pricing the platform derives for them: cancelReinstateDerivedPricing. Additive for reads, and a new refusal for saves. Every quote now has cancelReinstateDerivedPricing, which is read-only: writes ignore a value you send. On an open cancellation or reinstatement quote, every save stores it: Create Entity, Update Entity, an import, and Clear Quote Rating. It is the pricing binding the quote derives, before the charges the quote adds: each charge at what it has earned by the cancellation date, or the pricing the policy had before its cancellation, with the tax adjustment InsCipher sources where your company uses it. fullTermPricingInfo is unchanged. The field is null on every other quote, on a cancellation or reinstatement quote that names no date yet (it still saves, and the field fills once the date is set), and on a quote not saved since this change. A cancellation or reinstatement quote without referencingPolicy is refused with 400 EntityInvariantViolation, as before. Generate Quote from Policy returns it on a CANCEL or REINSTATE draft. A save that cannot calculate it is now refused, and nothing is saved: 422 cancel-reinstate-pricing-unavailable, with a message that starts “Unable to calculate cancellation pricing:” or “Unable to calculate reinstatement pricing:” and says why. Examples: a cancellation dated where the policy is no longer active, such as a second cancellation; a date outside the term; a reinstatement of a policy whose latest transaction is not its cancellation, such as one endorsed after its cancellation before that was refused. Generate returns the same 422 for such a draft. Where your company sources taxes from InsCipher, a save can also return 502 cancel-reinstate-pricing-unavailable when InsCipher does not answer (retry), and 409 cancel-reinstate-pricing-changed when the policy changes while the quote saves (save again). A quote that can no longer be saved can still be cancelled. Every quote read now includes the field, so an expectedEntityHash computed from a quote read before this change no longer matches: read the quote again. A configuration import, validation or patch must declare the Quote field cancelReinstateDerivedPricing, as every configuration has since October 26, or it reports FRAMEWORK_REQUIRED_FIELD_INVALID. A configuration import or patch into a company with no records accepts a type or cardinality change again. Import Configuration and Patch Configuration skip their breaking-change check when the company holds no stored records. Since October 24, 2026, the search index the platform keeps for every company counted as one, so such a company was refused with 409 fmv1-config-breaking-changes, dry runs included, unless an import sent ?force=true. The search index no longer counts. A company that holds records is refused as before, with the same code, status and message.

October 26, 2026

payeeFinancials in person and organization export columns, with invoiceTypeScope; import refuses it elsewhere. Additive for these two surfaces: a person or organization export column can read payeeFinancials, the person’s or organization’s balances as a payee across every invoice that names them as payee, at the start and end of a reporting period. payeeFinancials.end.payeeFinancialsRows carries one row per Event invoice type and per Policy invoice type, deprecated ones included, Event types first, with invoiceType, invoiceTypeScope ("event" or "policy"), expectedDirection, paid, owed, total (paid plus owed) and lineItems, in the plain sign. A row has no reserves, even on a reserved Event type: a reserve belongs to the claim, not the payee. Each invoice counts toward its current payee, so a re-linked invoice counts toward its new payee at every date, and a deleted invoice nets to 0. The period ends on asOf, else today, and starts at the beginning of time, so payeeFinancials.start.date is null and every start figure is 0. A person or organization with no invoices totals 0. FINANCIAL_CATEGORY_TOTAL and TYPE_SIGN read its rows; FINANCIAL_CATEGORY_TOTAL takes a new option, invoiceTypeScope: "event" | "policy", which narrows a total over payeeFinancials rows to one scope’s invoice types, for example FINANCIAL_CATEGORY_TOTAL(payeeFinancials.end.payeeFinancialsRows, "paid", ["ALAE"], { sign: "type" }) and FINANCIAL_CATEGORY_TOTAL(payeeFinancials.end.payeeFinancialsRows, "owed", ["Commission"], { invoiceTypeScope: "policy" }). Configuration import checks FINANCIAL_CATEGORY_TOTAL names over payeeFinancials rows against both Event and Policy invoice types, or the one invoiceTypeScope names. It refuses, as errors, "reserves" there, a name that is a live Event invoice type and a live Policy invoice type without invoiceTypeScope, invoiceTypeScope over any other source, payeeFinancials in any smart tag or any other export column (person and organization item rows included), payeeFinancials read off a related-event or related-policy row, and a Person or Organization field named payeeFinancials. See the JEXL reference. Docs: Rate, Quote, Bind. A new guide, Rate, Quote, Bind, takes a quote from creation to a bound policy with one request per step: create the quote with its pricingSetup, rate it with saveToQuote: true, complete it with invoices: "generate", and bind it with invoices: "saved". It also covers binding an in-progress quote with invoices: "generate", the Invoicing Off journey, and a company that does not invoice. The Roadmap now lists hosted rating as available. Rate Saved Quote by Id can save the rating: saveToQuote. Additive. Rate Saved Quote by Id takes saveToQuote: true, which saves a successful rating to the quote as the app saves it: what the rating workflow owns, fullTermPricingInfo included, with the quote’s calculated fields recalculated, so fullTermBillingInfo follows the new pricing. The run becomes the quote’s current rating, so Bind Quote and Check Bind Conditions check whether it is out of date as they check a rating saved in the app. Saving needs quote.edit beside quote.rate. A bound or cancelled quote returns 409, and a quote another request changed while it was being rated returns 409 quote-changed-during-rating. A failed rating saves nothing, and a rate without saveToQuote is unchanged. See Persisting rating results. Complete can generate a quote’s invoices: invoices: "generate". Additive, for companies that invoice. The Update Entity that completes a quote takes invoices: "generate" beside quoteStatus: "complete": it generates the quote’s invoice plan as Generate Quote Invoices does and saves it, then completes the quote, checking that plan. A refusal of either saves neither. invoices on an update that does not complete a quote, or with another value, returns 400 INVALID_COMPLETE_INVOICES; on a quote already complete, 409 quote-already-complete; at a company that does not invoice, 403 finv2-policy-invoicing-disabled. See Completing. Refusals name the request that fixes them. Messages only; codes and shapes are unchanged. A refusal that asked you to rate a quote and save the results with two requests now names one, POST /api/v1/companies/{companyId}/quotes/{quoteId}/rate with saveToQuote: true: the missing-billing refusals of Generate Quote Invoices and of a bind with invoices: "generate", the intended-layer refusals for missing pricing or billing, and Clear Quote Rating’s 422 quote-rating-not-clearable. 409 quote-rating-out-of-date and Check Bind Conditions’ rating advisories name it too, before confirmStaleRating: true. An intended-layer refusal for missing invoices names invoices: "generate": on the bind, or on the update that completes the quote when Complete refuses. A quote’s create can state its pricing setup; pricingSetup and invoices are reserved Quote field names. Additive for the create, and only for companies that invoice. Create Entity for a quote takes one key beside the quote’s fields, pricingSetup: the quote’s pricingStackIntendedLayer and, optionally, its invoiceSettings, each shaped exactly as Set Quote Pricing Stack Intended Layer and Replace Quote Invoice Settings take them. Each is saved as its endpoint saves it, with the same refusals and audit events, in the same transaction as the quote, so a refused setting creates nothing; it needs quote.edit beside quote.create, and a company that does not invoice returns 403 finv2-policy-invoicing-disabled. Without invoiceSettings the quote uses your company’s payment defaults, and a create without pricingSetup is unchanged. With an Idempotency-Key, pricingSetup is part of the body the key identifies. It is not a quote field: entity reads never return it, and a quote update that sends it returns 400 PRICING_SETUP_CREATE_ONLY. Configuration import, validation and patches now refuse a Quote field named pricingSetup or invoices, because quote requests take options under those names; no configuration had one. See Reserved Quote field names. Breaking: two error codes say cancel/reinstate instead of lifecycle. A request that prices a cancellation’s or reinstatement’s tax through InsCipher, such as Cancel Policy Transaction, Reinstate Policy Transaction or binding a cancellation quote, now returns 422 inscipher-cancel-reinstate-tax-unpriceable where it returned 422 inscipher-lifecycle-tax-unpriceable. A configuration import, validation or patch that puts a tenant row in the QUOTE_TO_POLICY × CANCEL or REINSTATE conversion rules reports conversion-rule-closed-cancel-reinstate where it reported conversion-rule-lifecycle-only. Error messages that called these two transactions lifecycle transactions now call them cancel/reinstate transactions. The same requests succeed and fail as before; only the names changed. Breaking: a cancelled policy can’t be endorsed until it is reinstated. Cancel and reinstate only switch a policy off and on; every other change is an endorsement. While a policy’s latest version carries a cancellation, whether it has taken effect or is still to come, endorsing it returns 409 PolicyCancelled, with a message that names the policy and says to reinstate it first: Endorse Policy Transaction; Generate Quote from Policy with transactionType: "ENDORSE"; Create Entity for an endorsement quote (one created already bound or cancelled is not refused); and binding an endorsement with Bind Quote or Bind Quote by Reference or by Value, by reference or by value, which answers it before its “no changes” and intended-layer refusals and, for a saved quote, before its invoice, billing and rating refusals (a cancellation also puts an endorsement’s rating out of date). Completing an endorsement quote of a cancelled policy with Update Entity returns the same 409, and Check Bind Conditions reports it as a payload failure, whether or not your company invoices. Other edits to an endorsement quote that is already open are still saved, and renewing a cancelled policy is unchanged. To change a policy and cancel it, endorse it and then cancel it; to reinstate it with changes, reinstate it with Reinstate Policy Transaction and then endorse it. Before, an endorsement could land on a cancelled policy, and a later reinstatement then restored the cancellation’s version instead of the pre-cancel one. See Cancellation and Reinstatement. The entity configuration schema advertises a Percentage inside a custom object as an integer. Get Entity Configuration, Get Policy Configuration and the MCP get_entity_schema tool published a Percentage sub-field of a custom object, such as a broker’s brokerDetails.defaultCommissionPercentage, as {"type": "string"}. They now publish it as a top-level Percentage field is published: "type": "integer", the canonical scaled integer (send 15% as 150000), with the same description. A string sent there is still accepted, but its scale depends on how your configuration’s cards enter that field ("15" is 15% under whole-number entry and 1500% under decimal entry), so send the integer. What a write accepts and stores is unchanged. Conversion rules accept SUBMISSION_TO_QUOTE and QUOTE_TO_SUBMISSION. Additive. A conversionRules row in a configuration import, export, validation body, and the add-conversion-rule-row, update-conversion-rule-row and delete-conversion-rule-row patch changes can name two new directions. SUBMISSION_TO_QUOTE pre-fills a Quote from its Submission and copies the fields both declare by default; QUOTE_TO_SUBMISSION writes chosen Quote values back to the Submission and copies nothing beyond its rules. Both pair only with NEW_BUSINESS and RENEW; import refuses either under another transaction type with conversion-rule-illegal-transaction. No transaction runs these rules yet, and configurations without them import and convert as before. See Conversion Rules.

October 24, 2026

filterText searches a record’s searchable fields, and policies can be searched. filterText on List Entities, entity export runs and the MCP search_entities tool matches the whole phrase as a substring of any of the record’s searchable fields: the fields declared in the configuration’s searchFields rows, a record’s display name, and an Event’s reference ID. A Submission’s submissionName and a Person’s or Organization’s email require searchFields rows. It ignores case, accents and repeated spaces, so jose finds “José”, and a phrase typed like a phone number also matches its digits. Words are not matched separately: acme freight finds “Acme Freight Inc” but not “Freight, Acme”. Additive: List Policies, policy export runs and the MCP list_policies tool take filterText, which matches a policy when any segment of its latest version holds the phrase. exposureRelatedEvents and exposureRelatedPolicies in exposure export columns; import refuses exposureRelatedPolicies on other form types. Additive for the exposure export: an exposure export column can read the insured’s exposureRelatedEvents and exposureRelatedPolicies, the same lists an Insured form reads for that exposure. The period always ends today and starts at the beginning of time, whatever asOf or startDate the request carries. exposureRelatedEvents holds the three lists exposureEvents, policyEvents and policyEventPrimaryExposure, read with FILTER_RELATED_EVENTS. Each row carries the keys a policyRelatedEvents row carries, with the Event’s eventFinancials as of today, and no paid, reserves, incurred or per-category money keys. An Event reported after today is left out. exposureRelatedPolicies holds one row per policy that ever recorded the exposure: the fields of its latest version, its id, and its policyFinancials and policyEarnings as of today. For example, FINANCIAL_CATEGORY_TOTAL(FILTER_RELATED_EVENTS(exposureRelatedEvents, { eventTypes: ["claim"] }), "total", ["Indemnity"], { sign: "type" }), EARNINGS_TOTAL(exposureRelatedPolicies, "earned", ["premium"]) and COUNT(exposureRelatedPolicies). An exposure with no events or policies totals 0. Configuration import refuses, as errors, either root in any other export column (exposure item rows included), an Exposure field named after either root, a PICK over exposureRelatedEvents rows of a key those rows do not carry, and exposureRelatedPolicies in a Claim/Incident, Quote or Policy smart tag. It warns when an exposure export column reads a key such as paid off an exposureRelatedEvents row. Insured form tags and built-in exposure columns print as before. See the JEXL reference. eventReserveUpdates in Claim/Incident smart tags and event export columns; import refuses it elsewhere. Additive for these two surfaces: a Claim/Incident smart tag and an event export column can read eventReserveUpdates, the Event’s Reserve Updates table as rows at the start and end of a reporting period. eventReserveUpdates.end.eventReserveUpdatesRows lists the rows dated on or before the period’s end and eventReserveUpdates.start.eventReserveUpdatesRows the rows dated before its start: one row per reserve change, eroding payment and history reset, newest recorded first, with kind ("user", "erodingPayment" or "reset"), date (the reserve date or payment date; null on a reset), invoiceType, expectedDirection, change, previousTotal, newTotal, memo, recordedBy and recordedAt. change is the change in remaining reserves in the plain sign, so a reserve increase on a payable type is negative and an eroding payment on it positive; previousTotal, newTotal and memo are set on reserve changes only. A history reset hides its invoice type’s earlier rows at both ends and shows its own row with a change of 0, and a removed payment shows nowhere. On the event export the period ends on asOf, else today, and starts on startDate; without a startDate, start.date is null and the start rows are []. On a Claim/Incident form it ends on the form’s as-of date, else the day it is generated, and starts at the beginning of time. Count or pick rows with the list functions, for example COUNT(eventReserveUpdates.end.eventReserveUpdatesRows) or FIRST(eventReserveUpdates.end.eventReserveUpdatesRows).change for the latest change. An Event reported after the period’s end keeps its export row and its form, with null rows at both ends, so a count prints 0 and a row read prints blank. Configuration import refuses, as errors, eventReserveUpdates in any other smart tag or export column (Policy, Quote and Insured forms, the policy, exposure, bordereau, quote and submission exports, and item rows included) and an Event field named eventReserveUpdates. Built-in event columns and tags print as before. See the JEXL reference. New: DATED_BETWEEN and TYPE_SIGN, and reserve updates in smart-tag tables. Additive. DATED_BETWEEN(rows, from, to) keeps the eventReserveUpdates rows dated from from to to, both days included, in the order they came. A null bound is open, and a history reset, which has no date, is never kept. The result is null over rows that are not a list, or when a bound that is not null cannot be read as a day. For example, COUNT(DATED_BETWEEN(eventReserveUpdates.end.eventReserveUpdatesRows[.kind == "user"], eventReserveUpdates.start.date, eventReserveUpdates.end.date)) counts the reserve changes in an event export’s period. TYPE_SIGN(row, field) returns one row’s figure in its invoice type’s own direction: negated on a payable type and unchanged on a receivable one. It reads one row of eventReserveUpdates, eventFinancials or policyFinancials rows, so FORMAT_CURRENCY(TYPE_SIGN(row, "change")) prints a reserve increase on a loss type as a positive amount. It is null when the figure is not a number or the row states no known expectedDirection. A Claim/Incident smart tag’s SMART_TAG_TABLE can now list eventReserveUpdates rows, filtered, sorted or narrowed with DATED_BETWEEN, and import checks its columns against the reserve update row’s keys. A column over rows that state an expectedDirection can call TYPE_SIGN(row, …). Configuration import refuses, as errors on every surface, DATED_BETWEEN over any other rows, with other than three arguments, with a literal bound that is not null or a real YYYY-MM-DD day, or in a table column or subtotal. It also refuses TYPE_SIGN with other than two arguments, with a field that is not a string literal naming a number field of the row, or over anything that is not one such row, such as a whole list, a related event row or a line item. See the JEXL reference.

October 23, 2026

Fields can declare searchable and showInSearchResults. Additive. A field in a configuration import, export, describe/fields row, and the add-field and update-field patch changes takes two optional booleans: searchable, whose values search will find the record by, and showInSearchResults, a value shown on the record’s global search result. A custom-object sub-field takes searchable, which applies wherever the object is embedded; showInSearchResults is refused there. Import refuses searchable: true on a Boolean, Object, Join, Pointer or EmbeddedExposure field, and warns that searchable: false on a record’s display-name field changes nothing, since a record is always found by its name. update-field clears either declaration with null. Search does not read the declarations yet; configurations that declare neither import and export unchanged. Breaking: invoices carry a transactionId, and invoiceNumber is now the sender’s number. Every Financials invoice row, from List Invoices, Get Invoice, each single-invoice write, Import Event Financials and Update Policy Invoices, gains transactionId: the INV-XXXXXX-XXX Transaction ID the server generates for each invoice, the value invoiceNumber used to hold. invoiceNumber now holds the number printed on the invoice by its sender, a string or null, and need not be unique. Create Invoice and Import Event Financials accept an optional invoiceNumber per invoice: it is trimmed, a blank value means none, and more than 255 characters returns 400. A retry of the same actionId with a different invoiceNumber returns 409 ACTION_ID_REUSED. Update Invoice accepts it too: leaving it out keeps the invoice’s number, null or blank clears it, and a string sets it. Only a draft may change it; on a posted invoice, a change or clear returns the new 422 INVOICE_NUMBER_IMMUTABLE, while restating the current number succeeds. Every invoice plan takes an optional invoiceNumber on each create: Update Policy Invoices, a policy transaction’s invoicePlan, Attach Quote Invoice Plan (a saved plan carries it to the quote’s bind), and the invoicePlan of Bind Quote by Reference or by Value. Left out, the new invoice’s invoiceNumber is its own transactionId; stated, it is that value, trimmed, or none for null or blank, so a policy invoice brought over from another system can carry that system’s number. More than 255 characters returns 400, and the number is fixed once the invoice is created, because a plan creates its invoices posted (Invoice plans). On List Invoices, number now matches invoiceNumber exactly and may match several invoices, and the new transactionId parameter matches the Transaction ID. The 409 DeleteBlockedByFinancials body from DELETE /policies/{policyId}, and the 409 an entity delete returns when financial records block it, rename financialBlockers.sampleInvoiceNumbers to financialBlockers.sampleTransactionIds, which lists the same Transaction IDs. Invoices that existed before this change read invoiceNumber: null until a one-time migration, run right after the release, fills it in: one created on a policy and still linked to that same policy, never moved, then reads its transactionId there, and every other one stays null. To keep identifying invoices as before, read transactionId where you read invoiceNumber, filter with transactionId where you filtered with number, and read sampleTransactionIds where you read sampleInvoiceNumbers. For imports: the attempt is stored first, and an entity write commits with its writeResult. Import Entity and Retry Import now store the import attempt before its transformations run, with every result null, and save each step’s result on it as the step completes. The entity create or update and the attempt’s writeResult are saved in one transaction, so an attempt whose guards passed and whose writeResult is null wrote no entity: it is in progress, or it stopped on an unexpected error. Before, the attempt was stored only after the write, so an unexpected error after a write left the entity with no attempt, and a retry could create it again. An unexpected error now keeps the attempt with the results it reached. Additive: an attempt carries updatedAt, when a result was last saved on it, in every response that returns one. The 201 responses still return the finished attempt, and are otherwise unchanged. See Reading an attempt. For events: previousClosedOn follows the order of the open/close history, and PREVIOUS_CLOSE. An event’s previousClosedOn is now the effective date of the close its eventOpenCloseHistory lists just before the reopen that reopenedOn reports, and it is empty when no close is listed before that re-open. Before, it was the latest close dated on or before the re-open, so an event re-opened and closed again on the same day reported that last close instead of the one before the re-open. A history whose dates run backwards follows the list order too: the close listed before the re-open is reported even when it is dated after it. Other events report the same date as before. A stored value changes when its event is next saved, and a one-time refresh updates the rest. Additive: the new pure JEXL function PREVIOUS_CLOSE(history) returns that close entry, or null, and previousClosedOn is calculated as PREVIOUS_CLOSE(eventOpenCloseHistory).effectiveOnDate. See the JEXL reference. Breaking, for events: openedOn and closedDate on create build the open/close history. Create Entity for an event accepts openedOn, the day the claim opened, and builds the event’s eventOpenCloseHistory from it and closedDate: an open entry on openedOn (on the day of creation in America/New_York when it is not sent), then a close on closedDate, which closes the event. Each entry’s actionTakenDate is the day of creation. Before, a sent openedOn was ignored, and a closedDate was stored on an event that stayed open with no close entry. Both dates take what Close Event’s effectiveOnDate takes: a real day in the years 1900–2200, in a supported timezone. A closedDate earlier than the day the event opens now returns 400 InvalidFieldModelV1Data and creates nothing, so a claim closed in the past needs its openedOn. An openedOn or closedDate sent with an eventOpenCloseHistory must agree with it (its earliest open entry; a last entry that is a close on that day), or the create returns 400. A log built from the dates is a write of eventOpenCloseHistory, so if your configuration gives that field a field write group, the create needs that group or returns 403 field-write-permission-required. The entity configuration schema marks openedOn with x-create-input: true, which the MCP get_entity_schema tool shows as createInput, and gives openedOn, closedDate and eventOpenCloseHistory a description of these rules. A create with neither date, or with only a log, is unchanged. See the Event Lifecycle overview. Breaking, for events: a Closed Date edit moves the open/close history. An Update Entity PATCH of an event’s closedDate (the edit the claim page’s Closed Date field makes) now moves the log’s last close to the new date, keeping the date the close was entered, or adds a close when the log does not end in one. The status, closedDate and the log then agree, and a later Re-open is bounded by the edited date. Four edits that used to succeed are now refused and change nothing: a date earlier than the event’s latest open or re-open (409 EventLifecycleDateOutOfOrder), clearing closedDate on a closed event, setting it on an open one, and changing it on a closed event with no log (all 400 InvalidFieldModelV1Data: re-open or close the event instead, or give it its log with Replace Event Open/Close History). The new date must also be a real day in the years 1900–2200, in a supported timezone. Moving the log is a write of eventOpenCloseHistory, so if your configuration gives that field a field write group, the edit needs that group or returns 403 field-write-permission-required. Before, the edit changed only the field, and Re-open still read the old close date. Sending the date the event already holds, in any timezone, is not an edit, and clearing a closedDate left on an open event is still accepted. Close Event, Re-open Event and Replace Event Open/Close History are unchanged. See Editing the Closed Date. Breaking, for events: a PATCH that sends eventOpenCloseHistory sets the status from it. The log must be complete and well formed, as Replace Event Open/Close History requires (an open entry, then alternating close and reopen, effective dates never decreasing), or the update returns 400 InvalidFieldModelV1Data; an empty log or null is refused too. The event’s eventStatus and closedDate then follow its last entry, so such a PATCH can close or re-open the event, and a closedDate sent with it must agree. Before, the log was stored as sent and the status and closedDate were left as they were. Sending the log the event already holds is not a write: the status and closedDate stay as they are. Sending the log still needs any field write group your configuration gives eventOpenCloseHistory, even unchanged, as before. A PATCH that sends neither the log nor closedDate is unchanged. New: retry an import. Additive. Retry Import (POST /api/v1/companies/{companyId}/imports/retry) runs a new attempt on a stored import row, so a failed import is fixed without sending the data again. Send importRowId to retry the row with new transformations and guards, or importAttemptId to retry that attempt’s row with the attempt’s own; sending transformations or guards with an importAttemptId, or both ids, or neither, is a 400. The row’s data and entity type are used as stored. The new attempt carries the request’s attemptBatch, and every retry of a row you can import answers 201 with importRowId and the whole importAttempt, whether or not the entity was written. It requires the stored row’s entity type’s import permission, such as submission.import: a row of a type you cannot import is a 404, as an unknown id is. It takes no Idempotency-Key. New: import an entity. Additive. Import Entity (POST /api/v1/companies/{companyId}/imports) imports one record from another system in the shape you hold it. It stores data as an import row first, then applies your transformations (one JEXL expression per field, or a mapOver mapping for a list of child objects), runs your guards, and creates the entity, or updates it when the transformed entity carries an id. Every request that passes validation answers 201 with the stored row’s importRowId and the whole importAttempt, whether or not the entity was written: a refused write is the attempt’s writeResult. A malformed request is a 400 and stores nothing. It requires the target entity type’s import permission, such as submission.import. It takes no Idempotency-Key.

October 22, 2026

New: list and get import rows. Additive. List Import Rows (GET /api/v1/companies/{companyId}/imports/rows) returns a page of the company’s import rows, newest first, filtered by entityType and importBatch, with 1-based page and pageSize (default 50, clamped to 100). Get Import Row (GET /api/v1/companies/{companyId}/imports/rows/{importRowId}) returns one. An import row is the data an import request received, stored before any transformation. Each entity type’s rows need that type’s import permission, such as person.import: the list shows only the types you may import, an entityType filter you may not import is a 403, and getting such a row is a 404. New: list and get import attempts. Additive. List Import Attempts (GET /api/v1/companies/{companyId}/imports/attempts) returns a page of the company’s import attempts, newest first, with 1-based page and pageSize (default 50, clamped to 100), filtered by importRowId, entityType, action, importBatch (the attempt’s import row’s batch, so a batch’s retries are included) or attemptBatch. Get Import Attempt (GET /api/v1/companies/{companyId}/imports/attempts/{importAttemptId}) returns one attempt by id. Each attempt is the whole record: the transformations and guards it ran with, and its transformationResult, guardResult and writeResult. Each entity type’s attempts need that type’s import permission, such as person.import: the list shows only the types you may import, an entityType filter you may not import is a 403, and getting such an attempt is a 404. Rating freshness checks are on for every company. They have been since October 21, 2026, and are no longer turned on company by company. Check Bind Conditions always returns advisories, possibly empty: one for each rating workflow whose rating is out of date, which never changes readyToBind. Bind Quote and the by-reference form of Bind Quote by Reference or by Value refuse an out-of-date rating sent without confirmStaleRating: true with 409 quote-rating-out-of-date, naming the workflows in outOfDateWorkflows. Before, a company without the checks got no advisories, and its binds were never refused for an out-of-date rating. A rating that is current, or whose freshness cannot be checked, such as a quote rated and saved only through the API, never needs confirmStaleRating.

October 21, 2026

A rating that read a field your configuration has since retired reads as a changed rating setup. Where rating freshness checks are on for your company, the rating advisories Check Bind Conditions returns and the 409 quote-rating-out-of-date message from Bind Quote and the by-reference form of Bind Quote by Reference or by Value said the inputs the rating workflow read had changed. They now say its setup has changed since it was rated, since no one can see or edit a retired field. The code, the status, outOfDateWorkflows and when a bind is refused are unchanged. Out-of-date rating messages name each change, and an endorsement’s newer policy version. Where rating freshness checks are on for your company, an endorsement whose policy has a newer version than the one it was rated against is out of date, as before, and the 409 quote-rating-out-of-date message from Bind Quote and the by-reference form of Bind Quote by Reference or by Value, and the rating advisories Check Bind Conditions returns, now say so with both versions: the policy the quote endorses has changed since the rating workflow “GL Pricing” rated it: version 3 is newer than version 2. They also name a moved term as the term (the term has changed since the rating workflow “GL Pricing” rated it), and a changed rating setup and a newer rater version beside changed inputs as what they are. Before, a newer policy version and a moved term read as changed inputs, and a rating whose inputs changed named nothing else. The code, the status, outOfDateWorkflows and when a bind is refused are unchanged. An endorsement rated in the app and saved without edits no longer reads out of date. Where rating freshness checks are on for your company, Check Bind Conditions returned a rating advisory for such an endorsement, and Bind Quote refused it with 409 quote-rating-out-of-date unless sent confirmStaleRating: true, when the new rating’s results differed from the policy’s, until the quote was rated again. It still reads out of date for the reasons any quote does, and once the policy it endorses has a newer version or its effective or end date moves. The code, the status and the message are unchanged. The 409 quote-rating-out-of-date message names confirmStaleRating: true as the way to bind. Where rating freshness checks are on for your company, Bind Quote and the by-reference form of Bind Quote by Reference or by Value refuse an out-of-date rating sent without confirmStaleRating: true, and the message advised rating the quote again and saving it. A rating saved again through the API can still read out of date, so following that advice could refuse the bind again. The message now names confirmStaleRating: true as the way to bind using the rating the quote holds, and no longer advises rating again or names the quote’s id and the rate and update paths; Bind Quote’s ratingOutOfDate example shows it in full. The rating advisories Check Bind Conditions returns say the same. The code, the status, outOfDateWorkflows and when a bind is refused are unchanged. Declared-table columns over exposureRelatedEvents rows can read the row’s eventFinancials. Additive. A SMART_TAG_TABLE column expression over exposureRelatedEvents rows can read row.eventFinancials and its sub-paths, as one over policyRelatedEvents rows already could, so a column can compare a claim’s own dated history with the form’s as-of date. With an Event list field eventOpenCloseHistory, LAST(SORT_BY(row.eventOpenCloseHistory[DAYS_BETWEEN(.effectiveOnDate, row.eventFinancials.end.date) >= 0], "effectiveOnDate")).action prints the last open or close action on or before that date. Configuration import refused it before. A column’s from and a table’s groupBy still cannot name it. Declared smart-tag tables can rule only between rows and bold their name-only rows. Additive. A SMART_TAG_TABLE layout accepts borders: "rows", a line under the header and between rows with no outer box and no column lines, and boldLabelRows: true, which prints bold every row whose first cell is its only one with text, such as a dealer or organization row above the rows that carry figures. Configuration import rejects a borders outside "all", "none" and "rows", a boldLabelRows that is not true or false, and either beside print: "paragraphs". Tables that state neither print as before. See the JEXL reference. Companies that do not invoice: Check Bind Conditions and Complete refuse an endorsement that changes nothing on the policy. Check Bind Conditions now reports such an endorsement quote as readyToBind: false, with one payload failure carrying the bind’s message, This endorsement has no changes to bind., and Complete (Update Entity moving quoteStatus to complete) refuses it with that 400 InvalidRequest, as Bind Quote already did. Before, Check Bind Conditions reported it ready and Complete completed it; only the bind refused. Companies that invoice have had this since an earlier change today. Endorsements that change the policy are unaffected, and an endorsement’s other transaction checks stay at the bind. finv2-invalid-quote-transaction-metadata names the field the quote lacks. Get Quote Invoicing, and the quote invoicing writes that return the same 400, said only that the quote was missing the transaction data its billing needs. The message now names the field (quoteType, referencingPolicy, the change’s date, policyStartDate or policyEndDate) and the Update Entity request that sets it. The code, status and when it is returned are unchanged. The 403 finv2-policy-invoicing-disabled message no longer says a policy’s invoices can’t change. Where a request needs policy invoicing and the company does not invoice, the message said policy invoices “cannot be created or changed”, but a policy’s existing invoices can still be voided with Update Policy Invoices. It now says so: no invoices can be planned or created, and existing ones can still be voided. The code, status and when it is returned are unchanged, except that an invoice batch, or an invoicePlan riding a policy transaction, that reaches its transaction after the company’s invoicing was turned off now returns this 403 instead of applying without the receivable check. Documentation fix — no behavior change. Delete Policy Transaction said the policy state reverts to what it was before the deleted transaction. The policy’s data, fullTermPricingInfo and fullTermBillingInfo do, and its written and earned premium follow, but invoices are not rolled back: an invoice the deleted transaction created stays active, and one it voided stays voided. exposureRelatedPolicies rows carry policyFinancials and policyEarnings, and both totals accept them. Additive for Insured smart tags: each exposureRelatedPolicies row now carries its policy’s policyFinancials and policyEarnings, read as of the form’s as-of date, else the day it is generated, from the beginning of time. FINANCIAL_CATEGORY_TOTAL and EARNINGS_TOTAL accept exposureRelatedPolicies rows — the list, filtered, or one row — and total each policy’s period-end rows, for example FINANCIAL_CATEGORY_TOTAL(exposureRelatedPolicies, "owed", ["Premium"]) or EARNINGS_TOTAL(exposureRelatedPolicies, "earned", ["premium"]). A declared table over exposureRelatedPolicies can total each policy in a column with row, and each group in a subtotal with rows. A list with no policies totals 0. A policy whose versions cannot be replayed has a null policyEarnings, and an EARNINGS_TOTAL over it, or over a list that holds it, is null, so the tag prints its empty text rather than the other policies’ total. Configuration import, which refused either macro over exposureRelatedPolicies, now accepts them: it checks names over these rows against Policy invoice types, refuses "reserves" there, and checks EARNINGS_TOTAL over them as over policyEarnings rows. EARNINGS_TOTAL over empty rows now totals 0 even when it names classifications, where it was null. An Insured form whose tags read only policy fields off the rows prints as before. See the JEXL reference. Companies that invoice: a new term sent straight to a policy must fit a pricing stack layer. New Business, Renew and bind by value of a NEW_BUSINESS or RENEW (Bind Quote by Reference or by Value) now return 422 policy-pricing-stack-layer-inconsistent, and save nothing, when data.fullTermBillingInfo is stated without data.fullTermPricingInfo. No layer of the pricing stack accepts billing without pricing, and a policy stored that way refused every later endorsement, cancellation or reinstatement sent without a saved quote, and every invoice batch, with 422 policy-pricing-stack-layer-change. Before, such a term was accepted when its receivable lines added up to zero (no lines, zero amounts, or payable lines only) and refused with 400 finv2-policy-billing-checksum otherwise; both now return this 422. State fullTermPricingInfo (an empty pricingComponents list prices the term at zero), or leave fullTermBillingInfo out. A saved quote’s bind is unchanged: its intended-layer check answers first. Companies that do not invoice are unaffected. Validate Policies passes cancellations and reinstatements with no pricing, and versionRegressions leaves out the pricing, billing and rating containers. Validate Policy and Validate Policies reported cancelEarnedPremium and cancelReinstatePremium as fail (“has no extractable premium”) for a cancellation or reinstatement that states no fullTermPricingInfo, such as one bound from a quote whose pricing was cleared, or any cancellation or reinstatement of a policy with no pricing. Such a version now passes, with no message. A pre-cancel version with no pricing counts as a premium of 0 against a reinstatement that states pricing, and a cancellation is judged on the earning history up to it, so a later reinstatement that binds no pricing no longer fails it. A version with no segments, or a stated fullTermPricingInfo without a numeric premium, still fails. versionRegressions no longer compares fullTermPricingInfo, fullTermBillingInfo, fullTermPolicyRatingResult or crossSegmentRatingOutputs, wherever they sit, so a cleared amendment or a move down the pricing stack no longer reports value-vanished or array-shrunk findings there; billingReceivableChecksum still checks stated billing against its price. Check names, statuses and the response shape are unchanged. A company that does not invoice can void a policy’s invoices, and a change to such a policy names that path. A policy invoiced while its company invoiced keeps those invoices after policy invoicing is turned off, and its company stores no billing, so an endorsement, cancellation or reinstatement of it was refused with 422 finv2-policy-restates-invoices, whose message suggested an invoicePlan or the bind’s invoices, both refused there with 403; nothing could void the invoices. Now Update Policy Invoices takes one plan while your company does not invoice: voidInvoices naming at least one invoice and no creates. It checks the plan against no billing, so the invoices may be voided one at a time; a plan that creates an invoice, an empty plan, or a pricingStackLayerChange still returns 403 finv2-policy-invoicing-disabled, with a message saying the batch only voids, and an invoice with payments still returns 422 LIVE_PAYMENTS, whose message now names its payments to remove. If invoicing is turned on while such a batch runs, it returns 409 finv2-policy-invoicing-changed and voids nothing. The change’s 422 finv2-policy-restates-invoices keeps its code and now names the batch, at every policy transaction, both forms of Bind Quote by Reference or by Value and Bind Quote; Check Bind Conditions reports it for an endorsement, cancellation or reinstatement quote as a payload failure, and Complete (Update Entity moving quoteStatus to complete) returns it, where both used to answer ready. Once no invoice is active, the change binds with no billing. The single-invoice endpoints’ 422 POLICY_INVOICE_BATCH_ONLY message now says a company that does not invoice sends only voids. Companies that invoice see no change. See A company that does not invoice. Check Bind Conditions and Complete refuse an endorsement that changes nothing on the policy. At companies that invoice, Check Bind Conditions now reports an endorsement quote whose bind would change nothing on the policy as readyToBind: false, with one payload failure carrying the bind’s message, and Complete (Update Entity moving quoteStatus to complete) refuses it with 400, as Bind Quote already did. An invoice plan is not a change to the policy, so when an endorsement’s only change is its plan, the message now names Update Policy Invoices for the policy, POST …/financials/policies/{policyId}/invoices/batch, at all three. Before, Check Bind Conditions reported such a quote ready and Complete completed it; only the bind refused. Endorsements that change the policy, and companies that do not invoice, are unaffected.

October 20, 2026

policyEarnings and EARNINGS_TOTAL in Policy smart tags and policy export columns; import refuses them elsewhere. Additive for these two surfaces: a Policy (quote-bind-flow) smart tag and a policy export column can read policyEarnings, the policy’s Earnings as pricing classification rows at the start and end of a reporting period, and total them with the new EARNINGS_TOTAL(source, figure, rollups, options?). policyEarnings.end.policyEarningsRows carries one row per classification of the company, deprecated ones included, plus any classification key the policy priced that the company does not have, with classification (the key), label, rollup (premium, taxes, fees, or null for a charge in no rollup), written (versions effective by the date), writtenAsBooked (versions booked by the date), earned and unearned (written minus earned), in dollars. The figures replay the policy’s versions, as the policy’s Earnings tab does, so they match it on every policy, including policies whose early versions have no stored earning layers. On the policy export the period ends on asOf (“View as of”), else today; on a Policy form it ends on the form’s as-of date, else the day it is generated. It always starts at the beginning of time, so policyEarnings.start.date is null and every start figure is 0. EARNINGS_TOTAL adds one figure over the rows in the named rollups, optionally narrowed with { classifications: [...] }, for example EARNINGS_TOTAL(policyEarnings.end.policyEarningsRows, "earned", ["premium"]); it is null before bind, when a Policy form’s policyEarnings is null, and when a named classification was deleted after import. Configuration import checks every EARNINGS_TOTAL call: the source must be policyEarnings.end.policyEarningsRows or policyEarnings.start.policyEarningsRows, the figure, rollups and options must be literals, and each classification key must be a classification of the company in a named rollup. It refuses, as errors, policyEarnings in any other smart tag or export column (Quote forms, the bordereau and Policy item rows included) and a Policy field named policyEarnings. Built-in policy columns and tags print as before. See the JEXL reference. policyFinancials in Policy smart tags and policy export columns; import refuses it elsewhere. Additive for these two surfaces: a Policy (quote-bind-flow) smart tag and a policy export column can read policyFinancials, the policy’s Balances as Policy invoice type rows at the start and end of a reporting period. policyFinancials.end.policyFinancialsRows carries one row per Policy invoice type, deprecated ones included, with invoiceType, expectedDirection, paid, owed, total (paid plus owed) and lineItems, in the plain sign; a row has no kind or reserves. On the policy export the period ends on asOf (“View as of”), else today; on a Policy form it ends on the form’s as-of date, else the day it is generated. It always starts at the beginning of time, so policyFinancials.start.date is null and every start figure is 0. An installment counts from its scheduled date, so one scheduled after the period’s end is not owed yet, and a policy that never invoiced totals 0, for example FINANCIAL_CATEGORY_TOTAL(policyFinancials.end.policyFinancialsRows, "owed", ["Premium"]). Before bind a Policy form’s policyFinancials is null, so a total over it prints blank. Configuration import checks FINANCIAL_CATEGORY_TOTAL names over policyFinancials rows against Policy invoice types and refuses "reserves" there; it refuses, as errors, policyFinancials in any other smart tag or export column (Quote forms, the bordereau and Policy item rows included) and a Policy field named policyFinancials. Built-in policy columns and tags print as before. See the JEXL reference. policyRelatedEvents in Policy smart tags and policy export columns; import refuses it elsewhere. Additive for these two surfaces: a Policy (quote-bind-flow) smart tag and a policy export column can read policyRelatedEvents, the policy’s Events as a list of rows. Each row carries id, eventType, eventStatus, eventReferenceId, eventName, eventDate, reportDate, closedDate, policyId, policyNumber, policyTermStart, policyTermEnd, the company’s Event fields, and the Event’s eventFinancials as of the period’s end; unlike exposureRelatedEvents rows, it carries no paid, reserves, incurred or per-category money keys. On the policy export the period ends on asOf (“View as of”), else today; on a Policy form it ends on the form’s as-of date, else the day it is generated; it always starts at the beginning of time. An Event reported after the period’s end is left out, and a policy with no Events totals 0, for example FINANCIAL_CATEGORY_TOTAL(policyRelatedEvents[.eventType == "claim"], "owed", ["ALAE"]). Before bind a Policy form’s policyRelatedEvents is null. Configuration import refuses, as errors, policyRelatedEvents in any other smart tag or export column (Quote forms, the bordereau and Policy item rows included) and a Policy field named policyRelatedEvents; FINANCIAL_CATEGORY_TOTAL names over it are checked against Event invoice types. Built-in policy columns and tags print as before. See the JEXL reference. eventFinancials in Claim/Incident smart tags and event export columns; import refuses it elsewhere. Additive for these two surfaces: a Claim/Incident smart tag and an event export column can read eventFinancials, the Event’s Financials Overview as invoice type rows at the start and end of a reporting period. eventFinancials.end.eventFinancialsRows and eventFinancials.start.eventFinancialsRows carry one row per Event invoice type, with invoiceType, kind, expectedDirection, paid, owed, reserves (reserved types only), total and lineItems, in the plain sign; eventFinancials.start.date and eventFinancials.end.date are the period’s dates. On the event export the period ends on asOf, else today, and starts on startDate; without a startDate it starts at the beginning of time, where start.date is null and every start figure is 0. On a Claim/Incident form it ends on the form’s as-of date, else the day it is generated, and starts at the beginning of time. Total the rows with FINANCIAL_CATEGORY_TOTAL, for example FINANCIAL_CATEGORY_TOTAL(eventFinancials.end.eventFinancialsRows, "paid", ["Indemnity"]) - FINANCIAL_CATEGORY_TOTAL(eventFinancials.start.eventFinancialsRows, "paid", ["Indemnity"]) for the paid in the period. An Event reported after the period’s end keeps its export row and its form, with null rows at both ends, so each total prints blank; a difference of two totals, such as the paid in the period, prints 0 unless the expression checks IS_PRESENT(eventFinancials.end.eventFinancialsRows) first. Configuration import refuses, as errors, eventFinancials in any other smart tag or export column (the bordereau, Policy item rows and the quote and submission exports included), exposureRelatedEvents in a Claim/Incident smart tag, where it was a warning, and an Event field named eventFinancials. Built-in event financial columns and tags print as before. See the JEXL reference. Breaking, where documents print financials in the “Transaction type direction” frame: FINANCIAL_CATEGORY_TOTAL states its sign. FINANCIAL_CATEGORY_TOTAL(source, figure, invoiceTypes, options?) now reads every figure in the plain sign, money in positive and money out negative, at every company. The Reporting tab’s “Financial tags in documents” setting no longer applies to it: add { sign: "type" } to print each invoice type positive in its expected direction, as that frame did. invoiceTypes may be a selector instead of a list of names, {} for every type or { expectedDirection: "payable" } / { expectedDirection: "receivable" }; options.lineItems narrows paid or owed to named line item types; and an Operating invoice type’s total, paid plus owed, may now be totalled. On an insured form a total reads the ledger as of the form’s as-of date, or the day it is generated, and exposureRelatedEvents leaves out an Event reported after that date unless it has a ledger entry by then. A total whose invoice type was retired or renamed after import prints nothing rather than failing the tag. Configuration import checks every call in smart tags, declared-table columns and export columns, and refuses sign: "type" over {} or over payable and receivable types together. On exposureRelatedEvents rows the financials map is replaced by eventFinancials, the Event’s Overview as invoice type rows. Built-in financial tags and columns, and the loss-run keys paid, reserves and incurred, print as before. See the JEXL reference.

October 19, 2026

Additive: Broker Portal request assignees in configuration. The brokerPortal section accepts an optional assignableUsers, a list of user ids that may not repeat. Patch Configuration accepts a new change kind, set-broker-portal-assignable-users, which replaces the whole list; it returns 400 when an id is not a user of your company or when the configuration has no brokerPortal section yet. Export includes the list only when it is set, so existing configurations export unchanged. When a broker submits a request on the portal, the Submission’s submissionAssignee is set from the list. See Broker Portal request assignees. Additive: a Broker Portal decline copy-to address in configuration. The brokerPortal section accepts an optional declineCopyTo email address, which the decline dialog prefills. Validate Configuration, Import Configuration and compare refuse a value that is not an email address. Patch Configuration accepts a new change kind, set-broker-portal-decline-copy-to, which sets the address or clears it with null. Export includes it only when it is set. See Broker Portal decline copy-to address.

October 16, 2026

Validate Policies refuses a bad date, policy ID or paging value with 400. Validate Policies returns 400 VALIDATION_ERROR naming the parameter when timeTravelBackToDate cannot be read as a timestamp (such as not-a-date or 2026-02-30), when a policyVersions pair’s policy ID is not a UUID, when offset is negative, or when limit is negative together with timeTravelBackToDate. Before, each returned 500 INTERNAL_ERROR. A policy ID must be in its hyphenated 8-4-4-4-12 form; a hyphenless or braced UUID is refused too, where before it returned 200 with every version checked against no segments. policyVersions requests still ignore limit and offset, and every other request behaves as before. A saved amending quote binds the pricing it holds. At companies that invoice, binding an endorsement, cancellation or reinstatement quote writes the quote’s fullTermPricingInfo onto the policy, null included, and a null fullTermPolicyRatingResult clears the policy’s. After Clear Quote Rating, or Update Entity with fullTermPricingInfo: null, the bind leaves the policy without pricing, at base when it has no billing or active invoices. Before, a saved quote’s null pricing was honored only when a request had saved that null explicitly; a quote whose pricing read null for another reason kept the policy’s. Now the quote’s own value decides. An endorsement quote that does not state the field keeps the policy’s pricing, and a cancellation or reinstatement quote with pricing binds the platform-derived pricing as before. Any intended layer can be chosen whatever the quote’s data is; Complete and Bind still refuse a mismatch. The policy’s earlier versions, earning history and ledger rows are unchanged. See The pricing stack. Breaking: pricing stack names are explicit. Get Quote Pricing Stack Intended Layer and Set Quote Pricing Stack Intended Layer now use GET and PUT /api/v1/companies/{companyId}/quotes/{quoteId}/pricing-stack-intended-layer, with pricingStackIntendedLayer in the request and response instead of intendedLayer. The old /intended-layer route is removed. Endorse, Cancel, Reinstate, bind by value of those transactions, and Update Policy Invoices now name a move in pricingStackLayerChange instead of layerChange; its { from, to } shape and the base, pricing, billing and invoicing values are unchanged. The three pricing stack refusals are now quote-pricing-stack-intended-layer-required, quote-pricing-stack-intended-layer-mismatch and policy-pricing-stack-layer-change, replacing the corresponding finv2-quote-intended-layer-required, finv2-quote-intended-layer-mismatch and finv2-policy-layer-change codes. Update endpoint paths, body and response keys, and error-code handling together. Validation, invoice behavior and stored choices are unchanged. Tasks refuse an id that is not a UUID with 400. Get Task, Update Task and Delete Task return 400 INVALID_TASK_ID when taskId is not a UUID. Create Task and Update Task return 400 VALIDATION_ERROR naming entity.id when it is not a UUID, and List Tasks does the same for its entityId filter. Before, each of these returned 500 INTERNAL_ERROR. A UUID must be in its hyphenated 8-4-4-4-12 form; a hyphenless or braced UUID is refused too. An empty entityId= is still ignored. Requests with valid UUIDs behave as before. Bordereau refuses a period bound that is not a date with 400. List Bordereau Rows and Start Bordereau Export Run return 400 INVALID_REQUEST naming periodStart or periodEnd when the value cannot be read as a timestamp, such as not-a-date or 2026-02-30. Before, the list returned 500 INTERNAL_ERROR and the export run was accepted and then failed. Every value that worked before still works, and an empty value is still ignored. List Notes refuses a bad page or pageSize with 400. List Notes now checks page and pageSize the way the files and folder list endpoints do. A value that is not a positive integer, including 0 or an empty value, returns 400 BadRequest naming the parameter, as does a page so large that it skips past row 9007199254740991. Before, page=0 and very large pages returned 500 INTERNAL_ERROR, pageSize=0 returned an empty 200, and an empty page= returned the first page. Valid requests return the same pages as before.

October 15, 2026

Breaking, for companies that invoice: a saved quote binds at its intended layer, and other changes name the layer they move to. Only companies with policy invoicing on are affected. A saved quote’s bind now lands exactly at the quote’s intended layer of the pricing stack: Bind Quote, the by-reference form of Bind Quote by Reference or by Value, Check Bind Conditions and Complete (Update Entity moving quoteStatus to complete) check it. A quote with no intended layer returns 422 finv2-quote-intended-layer-required, so set one with Set Quote Pricing Stack Intended Layer before you bind or complete, and a bind that would leave the policy at another layer returns 422 finv2-quote-intended-layer-mismatch; Check Bind Conditions reports either as a failure. No saved-quote bind body takes a layer. Endorse, Cancel and Reinstate, bind by value of an ENDORSE, CANCEL or REINSTATE, and Update Policy Invoices keep the policy at its layer unless the request names the move in the new layerChange, { "from": …, "to": … }. A change that leaves out fullTermBillingInfo on a billed policy, or whose plan voids every active invoice or gives a policy its first, returns 422 finv2-policy-layer-change until it names its move; the message names the layerChange to send. The invoice batch can name only invoicing to billing and back, and its body is now PolicyInvoiceBatchRequest, the plan plus layerChange. A layerChange naming one layer twice, or sent on a new term or a saved quote’s bind, returns 400; with invoicing disabled, 403 finv2-policy-invoicing-disabled. finv2-policy-billing-required and finv2-policy-invoices-required are retired: an amendment may now drop billing or void every invoice when its quote’s intended layer or its layerChange says so, and a plan that voids every invoice and creates none needs no billing. Attach Quote Invoice Plan no longer refuses a plan that voids every invoice. Validate Policy and Validate Policies still list billingContinuity, which now always reports pass: a stored version does not record the move its write named. See The pricing stack. Bind accepts confirmStaleRating. Additive. Bind Quote and the by-reference form of Bind Quote by Reference or by Value accept an optional boolean confirmStaleRating, which binds a quote whose rating is out of date, meaning an input its rating workflow read changed after it was rated, using the existing rating. Nothing else changes yet: rating freshness checks are turned on company by company. Once they are on for your company, a bind of an out-of-date rating without confirmStaleRating: true returns 409 quote-rating-out-of-date, naming the workflows in outOfDateWorkflows, and Check Bind Conditions carries advisories, one per out-of-date workflow, which never change readyToBind. A rating that is current, or whose freshness cannot be checked, such as a quote rated and saved only through the API, never needs it. The by-value form takes no confirmStaleRating; any other unknown key is still a 400. Declared smart-tag tables can group their rows. Additive. A SMART_TAG_TABLE layout accepts groupBy, a key the rows carry, and groupGap, 0 to 10 blank rows between groups. A column accepts subtotal, an expression evaluated once per group with the group’s rows as rows, such as FORMAT_CURRENCY(FINANCIAL_CATEGORY_TOTAL(rows, "paid", ["Indemnity"])) or the label '"Subtotals:"'. Groups print in the order their first row appears, each followed by one subtotal row when a column states a subtotal. Configuration import rejects a groupBy the rows do not carry, a groupGap outside 0 to 10, a groupGap or subtotal without groupBy, any of the three beside print: "paragraphs", and a subtotal that reads anything but rows or calls a function that is not pure. Tables that state none of these keys print as before. See the JEXL reference. Choose a quote’s intended layer, and clear a quote’s rating. Additive, and only for companies that invoice. Get Quote Pricing Stack Intended Layer and Set Quote Pricing Stack Intended Layer, GET and PUT /api/v1/companies/{companyId}/quotes/{quoteId}/intended-layer, read and save the layer of the pricing stack a quote is meant to bind at: base, pricing, billing or invoicing, or null while none is chosen. They require quote.view and quote.edit. The layer is stored on the quote apart from its fields, every quote starts with none, and binds do not check it yet. Clear Quote Rating, DELETE /api/v1/companies/{companyId}/quotes/{quoteId}/rating (quote.edit), clears what a saved quote’s rating workflow owns, pricing included, then saves the quote so its billing recalculates; the invoice plan is left alone. A quote that names no rating workflow returns 409 quote-has-no-rating, and a workflow that cannot be cleared safely returns 422 quote-rating-not-clearable. A company that does not invoice gets 403 finv2-policy-invoicing-disabled from all three, and a bound or cancelled quote refuses both writes with 409. See Quote invoicing. More refusals name their fix with your real ids. No behavior change: the same requests are refused with the same codes and statuses. These messages used to print {companyId}, {policyId} or {entityType} placeholders, and now carry your company id and the ids involved: 409 QuoteBoundImmutable, when you edit a bound quote, names the policy’s endorse path and the quote create path; 409 DeleteBlockedByFinancials, from the entity and policy deleteAll endpoints, names the financials deleteAll path; and 409 fmv1-config-breaking-changes, from configuration import, names the deleteAll path of each affected entity type. POLICY_INVOICE_BATCH_ONLY names the exact fix. No behavior change: the same writes are refused with the same 422. The message now names the policy invoice batch for the policy involved with your real ids, POST /api/v1/companies/{your company}/financials/policies/{that policy}/invoices/batch, and the invoice read that supplies each void’s headJournalId, rather than {companyId} and {policyId} placeholders. It also no longer says the batch rewrites a policy’s whole invoice set: a batch voids and creates only the invoices it names, and the policy’s other invoices stay as they are. Policy terms on related-event rows, and DISTINCT_BY. Additive. Each row of FILTER_RELATED_EVENTS(exposureRelatedEvents, …) and its three named lists now carries policyTermStart and policyTermEnd: the start and end dates of the event’s policy, read from its latest version with policyNumber. Both are empty when the event has no policy. They are dates, so SORT_BY orders by them and FORMAT_DATE formats them, and a declared table may name them in a column’s from or row. expression. The new pure JEXL function DISTINCT_BY(list, subFieldKey) keeps the first row for each distinct value of one sub-field, in input order, and drops rows whose value is empty; sort first to choose which row is kept. A declared smart-tag table accepts it as its rows, and its columns are checked against the rows it reads, so SMART_TAG_TABLE(DISTINCT_BY(SORT_BY(FILTER_RELATED_EVENTS(exposureRelatedEvents), "policyTermStart"), "policyId"), [{heading: "Policy period", expression: 'FORMAT_DATE(row.policyTermStart, "M/D/YYYY") + " - " + FORMAT_DATE(row.policyTermEnd, "M/D/YYYY")'}], {print: "paragraphs"}) lists each policy period once, in term order. An Event field of your own named policyTermStart or policyTermEnd no longer reaches these rows; the built-in values take those names. See the JEXL reference.

October 14, 2026

Idempotency keys replay for 30 days. A successful keyed request reserves its key for 30 days from the first attempt’s admission. An identical retry in that window replays the result without extending the deadline; a different request is rejected. Once 30 days have elapsed, the same key runs as a new request, even with a different body. Failed attempts reserve no key. This applies to keyed writes that use Idempotency-Key. Financials V2 actionId behavior is unchanged. An entity create’s Idempotency-Key no longer becomes the entity’s ID. POST /api/v1/companies/{companyId}/entities/{entityType} now mints the new entity’s id itself; the key only makes retries safe. A retry with the same key, entity type and body still returns 201 with the original entity’s id and created: false, and Idempotency-Owner and idempotencyOwnerMatched work as before. A request that failed stores nothing, so its retry runs again. Keys belong to the company, so any API key in the company can retry them. Reusing a key with a different entity type or body, or retrying a create whose entity was deleted, still returns 409 IDEMPOTENCY_KEY_REUSED. If you used the key as the entity’s ID before the response arrived, read the id from the response instead. Retries of creates made before this change still return their original entity. Retry policy writes with Idempotency-Key. POST /quotes/bind, POST /policies/transaction/new-business, and POST /policies/{policyId}/transaction/endorse accept an optional key. Repeat the same request with the same key to receive the original policy and transaction IDs without another policy version, invoice plan, or bind paperwork. A key reused with a different request returns 422 IDEMPOTENCY_KEY_REUSED; an invalid key returns 400 INVALID_IDEMPOTENCY_KEY. Without the header, these endpoints keep their existing behavior. Retry form uploads and replacements safely with Idempotency-Key. POST /forms/static, POST /forms/template, and their PUT /{number} replacements accept an optional key of 1 to 255 printable ASCII characters. Send the same key with the same request after a lost response to receive the original checkpoint’s current summary, with 201 for an upload or 200 for a replacement, without writing another version or storage object. A failed attempt stores no key. Keys belong to the calling credential. Reusing a key with a different path, query, or body returns 422 IDEMPOTENCY_KEY_REUSED; an invalid key returns 400 INVALID_IDEMPOTENCY_KEY. Requests without a key continue to create a new checkpoint on each call. Retry Drive exports, invoice bill reviews and parse triggers with Idempotency-Key. These three POST endpoints accept an optional 1–255-character printable ASCII key. Reuse the same key and request after a lost response to receive the original spreadsheet URL or run IDs without starting another operation. A different request under the same key returns 422 IDEMPOTENCY_KEY_REUSED; an invalid key returns 400 INVALID_IDEMPOTENCY_KEY. If task dispatch fails, retrying the key resumes the committed runs. Keyed parse requests use the key as the retry boundary: each distinct key starts a new parse run, even with force: false; calls without a key keep target dedupe. Drive delivery without a key still creates a fresh spreadsheet each time. A Google creation whose response is lost before the operation can be confirmed remains ambiguous if marker lookup cannot find the file, so automatic retry cannot guarantee that no duplicate file is made. A quote created through the API gets its recommended forms. POST /api/v1/companies/{companyId}/entities/quote now runs the company’s auto-added forms rules once against the new quote and attaches every matching form in rank order, as building the quote in the app does. Bind the quote and its policy forms carry the bound policy’s values, including the policy number. The rules don’t run again: an idempotent replay adds nothing, a form you remove stays removed, and a later PATCH adds no forms. Forms are added only when the caller also holds form.create and form.edit, and only for a quote created inProgress or complete. Send the new Auto-Add-Forms: false header to create a quote without them; any value other than true or false returns 400 INVALID_AUTO_ADD_FORMS and creates nothing. If the forms cannot be added, the quote is still created, and the response is unchanged either way. See Recommended forms on quote create. Retry note, task and folder creates safely with Idempotency-Key. Additive. POST /api/v1/companies/{companyId}/notes, POST /api/v1/companies/{companyId}/tasks and POST /api/v1/companies/{companyId}/folders accept an optional Idempotency-Key header of 1 to 255 printable ASCII characters. Send a new key with each create and the same key when you retry it: a retry after the first request succeeded returns 201 with the original id and creates no second note, task, assignee notice or folder. A request that failed stores nothing, so its retry runs again. Keys belong to the credential that sent them, so another API key, even one the same user created, cannot replay them. Reusing a key with a different query or body returns 422 IDEMPOTENCY_KEY_REUSED, and an invalid key returns 400 INVALID_IDEMPOTENCY_KEY; neither creates anything. Without the header, these creates behave as before.

October 13, 2026

Documentation corrections for policy invoicing. No behavior change: these pages now describe what the API already does. A policy-linked invoice changes only through the policy invoice batch, a policy transaction or a bind: Create (with links.policy), Update, Finalize, Void, Restore, Delete and Re-link refuse it with 422 POLICY_INVOICE_BATCH_ONLY, while payments, approvals and a payee change on an invoice without payments still work. Generate Quote from Policy now has a page, and it and Quote invoicing show how to change a bound policy through a saved quote: save the draft with referencingPolicy and its change date (endorsementEffectiveDate, cancellationEffectiveDate or reinstatementDate), rate an endorsement, and bind with invoices: "generate". At a quote’s doors, a billing change bound without invoices on a policy that has invoices, or a plan whose void is stale, voided or has payments, returns 422 finv2-quote-invoices-do-not-match-pricing, and the invoice batch answers an unknown policy with 422 finv2-policy-invoice-missing-aggregate, not 404. The batch’s response rows also carry approvedBy, which the invoice representation does not list. Every invoice plan create needs a payeeId, though the schema marks it optional, and should carry a scheduledDate, since a create without one counts as already sent and a later change keeps it. The financials endpoints no longer list a 404 for a company not enabled on the financials surface; no such gate exists.

October 12, 2026

Read, replace and delete a member’s export presets. Additive. GET /api/v1/companies/{companyId}/export-presets?ownerUserId= lists every saved export preset the named member owns. PUT /api/v1/companies/{companyId}/export-presets/{presetId} replaces a preset’s name, fieldKeys and rowSource (required; null for one row per record), validating them as create does; a name another of the owner’s presets uses is 409 ExportPresetNameTaken. DELETE /api/v1/companies/{companyId}/export-presets/{presetId} deletes a preset. They require export-preset.view-for-member, export-preset.edit-for-member and export-preset.delete-for-member, which the Admin role holds. POST /api/v1/companies/{companyId}/export-presets now accepts an optional rowSource for a one-row-per-item preset, whose fieldKeys may include that Object List’s item columns and __itemId / __itemIndex; an undeclared row source is 400 InvalidExportRowSource, and the response carries rowSource. See Export presets. Broker Portal pages. The brokerPortal section accepts an optional pages list naming the company’s Broker Portal pages: portalRequest, portalQuoteView, portalPolicyView, portalBrokerProfile and portalExposureCreate. A company has a portal page only when the list names it, so cards can be placed on it only then. Export keeps listing only app pages in the top-level pages. Validate Configuration, Import Configuration and compare refuse an app page key or a repeated key in brokerPortal.pages and name its path. Breaking, for a company whose Broker Portal is on: import and validate refuse a configuration whose brokerPortal.pages does not include portalRequest, and the portal cannot be turned on without it. See Broker Portal pages. OFAC screening can be limited to some exposure types. For a company using OFAC screening, the OFAC Screening placement can now name the exposure types it screens, as keys of the Exposure Types option set in its exposureTypes argument. An exposure of any other type is not screened: POST /api/v1/companies/{companyId}/ofac/screen returns 400 ofac-screening-exposure-type-not-screened and submits nothing to the provider, and writes to the exposure no longer start a screening record for it, so one that had no screening keeps ofacScreening: null. A screening the exposure already holds is kept and still returned. A placement without the argument screens every type, so no company’s behavior changes today. Validate Configuration, Import Configuration and Patch Configuration refuse an exposureTypes key that is not in Exposure Types, an empty list, and placements of one field that name different lists. The OFAC Screening overview also now describes the screening that writes start for a company with screening placed. Every company now follows the same billing rules. The companies that kept their earlier billing behavior until we moved them individually have moved, so the billing changes announced on October 8 now apply at every company. With policy invoicing disabled:
  • Quote.fullTermBillingInfo is the calculation NO_BILLING(). Quote writes ignore a value you send, quotes and policies hold null billing, and rate responses return it null.
  • A non-null fullTermBillingInfo sent to a policy transaction or a bind, or held by a quote you bind by ID, returns 403 finv2-policy-invoicing-disabled and saves nothing.
  • Cancellation and reinstatement no longer scale or restore billing, an endorsement no longer carries it, and billing cannot be an endorsement’s only channel.
  • Check Bind Conditions answers cancellation and reinstatement quotes and no longer returns 400 BindCheckUnsupportedQuoteType. Its invoices query now applies: generate or saved reports readyToBind: false, with the bind’s 403 finv2-policy-invoicing-disabled message as a failure, and any other value returns 400.
  • Validate Policy and Validate Policies return both billing checks, billingReceivableChecksum and billingContinuity, which pass.
  • POST /api/v1/companies/{companyId}/quotes/generate returns the finished draft with quoteType set.
Import Configuration, Validate Configuration and Patch Configuration hold every company to the same rules. Quote.fullTermBillingInfo must state its calculation, exactly NO_BILLING() while invoicing is disabled, and a blank or bare null calculation is refused with an error that names NO_BILLING(). No rating workflow may write fullTermBillingInfo or list billingPreconditions (with invoicing enabled, state billing conditions instead), and no Conversion Rule but the pinned billing copy may write fullTermBillingInfo. With invoicing enabled, the only change is that a bare null billing calculation is refused. See Quote billing calculations and The Billing Aggregate.

October 9, 2026

Shared fields copy by default; Conversion Rules are exceptions. When a quote is bound (new business, endorsement or renewal) and when POST /api/v1/companies/{companyId}/quotes/generate builds a draft (endorsement, renewal, cancellation or reinstatement), every field that Quote and Policy both declare with the same referenceId and type is now copied unchanged, unless a Conversion Rule for that direction and transaction type names it. Before, a field with no rule was not carried. A new rule expression, SKIP(), writes nothing to its destination. It is valid only as the whole expression: Validate Configuration reports conversion-rule-skip-not-whole-expression for one inside a larger expression, and Import Configuration and Patch Configuration refuse it with 400 config-validation-failed. CLEAR() still writes null. Every company’s configuration already has a SKIP() rule for each shared field it did not carry, so no bind or draft behaves differently today. From now on, a field you add to both Quote and Policy with the same referenceId and type carries at every conversion with no rule; add SKIP() where it should not. Deleting a rule on a shared field, a SKIP() rule included, turns the copy on for it. A plain source.<sameField> rule writes only what the copy writes, so you no longer need one. Calculated, system-managed and platform-owned fields, and the full-term containers, are never copied by default; a rule on a nested path such as mailingAddress.city stops the copy of the whole mailingAddress field in that scope. No framework rule writes a renewal draft’s policyEndDate: without a POLICY_TO_QUOTE RENEW rule for it, the draft takes the expiring term’s end date. Every current configuration states SKIP() for it; keep a rule for it in yours. See Conversion Rules.

October 8, 2026

Endorsement rating copies the other parts of the term from the policy. Rating an endorsement quote, in the app or through POST /api/v1/companies/{companyId}/quotes/{quoteId}/rate, rates the whole term. The part that contains the endorsement’s effective date is rated from the quote, as before. Every other part is now rated from the quote with each field that Quote and Policy share (same referenceId and type) replaced by that part’s Policy value, and calculated fields worked out again. Before, those parts were built by your POLICY_TO_QUOTE ENDORSE Conversion Rules, so a rule written for the endorsement draft could change the rating. Those rules no longer affect rating. A field your rating reads must therefore exist on both Quote and Policy, and your QUOTE_TO_POLICY rules must carry it unchanged at new business, endorsement and renewal; a Quote-only input is rated at the endorsement’s value across the whole term. Configuration validation does not check this. For every company that rates endorsements today, the results are unchanged. See Fields your rating reads. Additive, staff policy repair. The changes form of Repair Policies fieldValues now returns recordedDifferences on its dry-run and apply reports: the baseline key differences of each version whose differences the requested changes can all settle, by transaction, version, start date and key, with SHA-256 digests of both values as they stand after the changes, and one sha256 over the list. The list carries no values. Pass that digest as the new optional recordedDifferencesSha256 to accept exactly those differences: a version that holds them verifies when the changed history keeps them, and the new recordedDifferencesPinMatched says whether the pin still matches. With a pin, apply refuses with 422: policy-history-repair-baseline-difference-digest-mismatch when the pin no longer matches the history’s differences, policy-history-repair-baseline-rebuild-mismatch for a coded difference (a boundary, a segment count or a failed replay), policy-history-repair-baseline-difference-unrecordable for a key difference the changes cannot settle, policy-history-repair-baseline-difference-changed when the changes would add, remove or alter a difference in a version that holds recorded differences, and policy-history-repair-history-rebuild-mismatch when a difference appears in a version that holds none. Without the pin, apply refuses any baseline difference, as before. A proof version that kept recorded differences reports recordedDifferenceCount, and the reports’ declaredEffects are now documented. Separately, with or without a pin, a saved change that writes into an item of a top-level calculated List that the rebuilt parent lacks now replays from the previous stored version’s list, when that version holds one value for the field across the segment’s days, and is reported as a calculated-collection-seeded effect with the seeded value’s digest; such a history used to fail its replay and may now verify. A request now waits at most 5 s for each company lock it needs, and not at all while another transaction holds the company row: either returns 409 policy-history-repair-company-busy and changes nothing; retry it with backoff. Invoicing docs and refusal messages. A new guide, Invoicing a policy, shows the three doors that put invoices on a policy (a saved quote’s bind, a policy transaction or bind by value that sends its own invoicePlan, and the invoice batch after the bind) and walks five requests with the invoices each produces. A new Invoice plans page explains the plan, the rule that a policy’s active invoices add up to its billing, kept and voided invoices, credits, version checks, and every refusal with its fix. Each invoicing endpoint’s page now opens with its door and where its invoices come from. At the /api/v1 doors (the five policy transactions, both forms of POST /api/v1/companies/{companyId}/quotes/bind, Bind Quote, Check Bind Conditions, the quote invoicing endpoints and Update Policy Invoices), these refusals now name their fix through the API, with your request’s ids in any path they name: finv2-policy-restates-invoices, finv2-invoice-head-conflict, finv2-invoice-not-found (a void naming an invoice the policy does not have), finv2-policy-invoice-not-conserved, finv2-policy-batch-unpriced-charge, finv2-policy-invoice-missing-aggregate, finv2-policy-batch-no-pricing, finv2-policy-invoice-binding-refused, finv2-policy-invoices-required, finv2-policy-create-payee-invalid, which now names the field and the kinds of payee its invoice type accepts, LIVE_PAYMENTS when a plan voids an invoice that has payments, and, at the quote doors, finv2-policy-billing-required. A quote without billing gets one fix wherever a quote door finds it: rate and save the quote again, with the configuration export for its calculation. A plan line that does not add up now says which way it misses, “the plan bills 1 cent too much”, instead of a signed “off by -1”, and Replace Quote Invoice Settings names the kinds of payee an invoice type accepts when it refuses one. Only messages changed: codes, statuses and details are as they were. Update Policy Invoices is now listed in the Policy Invoicing section; its address is unchanged. POST /api/v1/companies/{companyId}/quotes/bind now has its own page, Bind Quote by Reference or by Value. Bind states its invoices. Breaking: what a bind invoices changes at invoicing-on companies, and generateInvoices is refused at every company. A quote owns its invoices, so its bind now says which. Bind Quote and the by-reference form of Bind Quote by Reference or by Value take invoices: "generate" generates them at bind from the quote’s invoice settings, as Generate Quote Invoices does, without saving them on the quote, and "saved" binds the plan saved on the quote. Leaving it out binds no invoices, and while the quote holds a saved plan it returns 409 finv2-quote-has-invoice-plan; before, the saved plan applied silently. "saved" with no plan saved returns 409 finv2-quote-invoice-plan-missing. Each refusal names its fix with the request’s own ids, and 422 finv2-quote-invoices-do-not-match-pricing now names each line that is off, which way, and the API’s fixes instead of the app’s Billing step (see Invoicing docs and refusal messages, above). generateInvoices is retired: both bind routes, by value included, refuse it as an unknown key with 400, false included. Send invoices: "generate" where you sent generateInvoices: true; by value, send an invoicePlan or nothing. Check Bind Conditions takes invoices=generate or invoices=saved to check the same bind, reports both 409s as payload failures, and no longer reads generateInvoices. With invoicing disabled, invoices returns 403 finv2-policy-invoicing-disabled, as generateInvoices: true did. See Quote invoicing. Quote invoicing, invoicing-on companies. Five new endpoints prepare a quote’s invoices before it binds. Get Quote Invoicing returns the quote’s invoice settings, its own or the company’s payment defaults (settingsSource says which), and the plan saved on it. Replace Quote Invoice Settings saves the schedule, payees and endorsement handling; a PUT replaces them all. Generate Quote Invoices generates invoices onto the quote from those settings and its saved fullTermBillingInfo, as the app’s Generate Invoices does, and keeps the settings it used. Attach Quote Invoice Plan saves a plan you wrote, after the checks a bind runs on it, and Clear Quote Invoice Plan clears it. A bind applies the saved plan when it says invoices: "saved" (above). With policy invoicing disabled, every endpoint returns 403 finv2-policy-invoicing-disabled. Writes to a bound or cancelled quote return 409 (QuoteBoundImmutable or QuoteNotEditable), naming the policy’s invoice batch or a new quote with your request’s ids. The app’s Billing step and these endpoints share one saved record, so a quote’s settings now stay when its plan is cleared, from either one. See Quote invoicing. Invoicing needs a real billing calculation. A company now counts as invoicing only when its policy invoicing is turned on and its Quote.fullTermBillingInfo calculation is something other than NO_BILLING(). Until both are true it behaves exactly as a company with invoicing disabled: quotes calculate null billing, a non-null fullTermBillingInfo sent to a policy transaction or a bind returns 403 finv2-policy-invoicing-disabled, and invoice changes (the policy invoice batch, invoicePlan, a bind’s invoices) are refused as they are with invoicing disabled. A company in the platform’s migrating registry may have invoicing turned on while it still states NO_BILLING(), and Import Configuration and Validate Configuration accept that calculation there; billing starts when a configuration change replaces it, from each quote’s next save. Elsewhere, invoicing on still requires a real calculation. Every company with invoicing on today already states one, so no current behavior changes. Additive: Broker Portal branding in configuration. The configuration document accepts an optional top-level brokerPortal section with branding.primaryColor, branding.accentColor (6-digit hex colours) and an optional branding.logoKey. Validate Configuration, Import Configuration and compare refuse a malformed value or an unknown key and name its path. Patch Configuration accepts a new change kind, set-broker-portal-branding, which replaces the whole branding value. Export includes the section only when it is set, so existing configurations export unchanged. The section does not turn the Broker Portal on, and the portal does not read it. See Broker Portal section. Check Bind Conditions reports the policy fields a bind requires. For new business and renewal quotes, Check Bind Conditions now checks the converted policy against the same required fields and types as the bind. A required policy field the quote leaves empty, such as a policyNumber copied from a quote that has none, now returns readyToBind: false with a payload failure carrying the bind’s 400 InvalidEntityShape message, for example 'policyNumber' is required but null. Before, the check answered readyToBind: true and the bind failed. A value your configuration calculates only at bind, such as a policy number generated from a sequence, is not checked, because it does not exist until the bind; the bind still refuses the policy if that calculation leaves the value empty. As at bind, these failures are reported once the policy passes its bind rules. Completing a quote does not check these fields. Documentation: where a bind gets required policy fields. No behavior change. Bind Quote and both forms of POST /api/v1/companies/{companyId}/quotes/bind now explain that a bind builds the policy only from Quote data, through your QUOTE_TO_POLICY Conversion Rules and the policy’s calculated fields. A required policy field such as policyNumber must therefore be carried on the quote, or calculated by your configuration at bind. Otherwise the bind returns 400 InvalidEntityShape naming the field. To bind by reference, set the value on the quote with Update Entity first. To bind by value, include it in data. Billing sent while invoicing is disabled is refused. At companies with policy invoicing disabled, a non-null fullTermBillingInfo sent to a policy transaction (New Business, Renew, Endorse, Cancel or Reinstate) or to a bind by value (POST /api/v1/companies/{companyId}/quotes/bind) now returns 403 finv2-policy-invoicing-disabled, and nothing is saved. Before, the value was silently dropped. On Endorse this includes billing inside a whole-policy deltas value. Binding by ID (Bind Quote, or quotes/bind with a quoteId) a quote that still holds billing is refused the same way. Quotes calculate null billing while invoicing is disabled, so only a quote saved before its company’s billing was cleared holds any, and saving it again (Complete does) clears it. Check Bind Conditions reports that refusal as a payload failure with readyToBind: false, never as a 403. Send null or leave the field out: the policy stores null, as before. Quote saves still ignore a sent value, and billing the platform derives or carries is unchanged. A few companies keep their current billing behavior until we move them individually. Invoicing-enabled behavior is unchanged. Bind by value accepts an invoice plan. The by-value form of Bind Quote by Reference or by Value accepts an optional invoicePlan, the explicit keep/void/create plan the policy transactions take (Invoice plans). It commits atomically with the bind and is checked the same way, so a by-value change that moves what an invoiced policy’s invoices must net to can restate them instead of returning 422 finv2-policy-restates-invoices. With invoicing disabled, an invoicePlan returns 403 finv2-policy-invoicing-disabled. It is the by-value form’s only way to create invoices; the form takes no invoices (above). Configurations keep the billing copy rule. Validate Configuration, Import Configuration and Patch Configuration now refuse, at every company, a configuration that drops or changes one of the three QUOTE_TO_POLICY conversion rules that copy the quote’s billing (NEW_BUSINESS, ENDORSE and RENEW, each writing fullTermBillingInfo from source.fullTermBillingInfo), as they already refuse one that drops or changes the fullTermPricingInfo copy. Validate reports conversion-rule-framework-row-missing or conversion-rule-framework-row-changed naming the rule; import (forced or not) and patch return 400 config-validation-failed, and nothing is saved. Every configuration carries these rules today, so no configuration needs to change and no bind behaves differently. The in-app Conversion Rules page marks each framework-pinned rule and offers no Edit or Delete for it. See Conversion Rules. Check Bind Conditions takes every quote type. At companies with policy invoicing disabled, Check Bind Conditions now answers cancellation and reinstatement quotes instead of returning 400 BindCheckUnsupportedQuoteType. The answer is 200 with checkScope cancellation-billing or reinstatement-billing, and readyToBind: true unless the quote still holds a non-null fullTermBillingInfo, which fails with the bind’s 403 finv2-policy-invoicing-disabled message, as on the other quote types. The scope covers billing only; the transaction’s other checks stay at bind. With invoices=generate or invoices=saved (above), the check reports readyToBind: false with one payload failure carrying the message of the 403 finv2-policy-invoicing-disabled that Bind Quote returns for invoices while invoicing is disabled. Leaving it out changes nothing, and any other value returns 400 VALIDATION_ERROR, as it does with invoicing enabled. The check never answers 403. A few companies keep their current billing behavior until we move them individually. Invoicing-enabled behavior is unchanged. Validate Policies includes the billing checks. Validate Policy and Validate Policies now return two more checks for each policy version: billingReceivableChecksum (the receivable lines of fullTermBillingInfo add up to fullTermPricingInfo.total) and billingContinuity (an endorsement, cancellation or reinstatement states billing when the version before it did). They are the checks a bind applies, run over stored versions. With policy invoicing enabled, a failure names the version and, for the receivable check, the signed gap between the price and the receivable lines. With invoicing disabled, both always pass, with a message that stored billing is not checked. A request without checks gains these rows and their summary counts. A checks list that names neither leaves them out rather than reporting them skipped, so it gets the same results as before. If you check result names against a fixed list, add these two. A few companies keep their current results until we move them individually. Generate Quote returns the finished draft. POST /api/v1/companies/{companyId}/quotes/generate now returns data with every field the company’s Quote configuration declares and every calculated field worked out as a save would, instead of only the fields its POLICY_TO_QUOTE Conversion Rules write. A field nothing fills is null, and so is a value that needs a saved quote, such as a generated quote number. fullTermBillingInfo is the company’s billing calculation, null while policy invoicing is disabled. Cancellation and reinstatement drafts keep their earned-floor and restored fullTermPricingInfo. Nothing is saved, as before. quoteType is now set from the request’s transactionType (ENDORSE gives endorsement, RENEW gives renewal, CANCEL gives cancellation, REINSTATE gives reinstatement), whatever a Conversion Rule writes, and calculated fields that read it are worked out for that transaction. validationIssues therefore no longer reports 'quoteType' is required but was not supplied; every other finding is unchanged. A draft with a value that fails its field’s type check comes back as before, without calculated fields, but with quoteType set. The app’s Endorse, Renew, Cancel and Reinstate wizards start from the same draft, so a calculated field that reads other records can show its value before the first save. A few companies keep their current Generate behavior, including the quoteType finding, until we move them individually. Version sequentiality checks the whole policy. Validate Policy with version above 1 reported versionSequentiality as fail on every policy (“Expected version 1 but found …”), because the check saw only the requested version. It now checks all of the policy’s versions, as a request without version does: it passes when they run 1, 2, 3 and so on with no gaps, and otherwise names the first gap. Validate Policies with policyVersions or timeTravelBackToDate gets the same fix. No other result changes. Validating a reinstatement on its own checks its premium. Validate Policy with version set to a reinstatement reported cancelReinstatePremium as fail (“REINSTATE at version …: expected CANCEL at version … but found no transaction.”), because the check saw only the requested version. It now compares the reinstatement’s premium with the version before the cancellation it reverses, as a request without version does. Validate Policies with policyVersions or timeTravelBackToDate gets the same fix. No other result changes. Documentation fix — no behavior change. Endorse and Policy concepts no longer call an endorsement that states only fullTermBillingInfo legal at every company. With invoicing disabled, a non-null value is refused with 403 finv2-policy-invoicing-disabled (above), so billing cannot be an endorsement’s only channel there. Invoicing-enabled companies, and companies awaiting their billing migration, accept a billing-only endorsement as before.

October 7, 2026

Billing target refusal wording, invoicing-on companies. When Bind Quote with generateInvoices: true, or Check Bind Conditions with generateInvoices=true, reads a quote whose fullTermBillingInfo is null (or has no lines), the refusal now names that field: “generateInvoices requires a billing target, but this quote’s fullTermBillingInfo is null. Its calculation and the company’s billing conditions are in the configuration export.” Bind still returns 422 finv2-payment-generation-refused, and the check still reports readyToBind: false with the message as a payload failure. Generating invoices for an existing policy with no stated billing keeps its current message. Invoicing-disabled behavior is unchanged. Additive, staff policy repair. The changes form of Repair Policies fieldValues now returns configHash on its dry-run and apply reports: the content hash of the company configuration the report was validated against, or null when the company has no configuration version. Pass it as expectedConfigHash when applying a reviewed preview. The rewrites form’s response is unchanged. Staff policy repair, invoicing-on companies. The legacy rewrites manifest of Repair Policies fieldValues now refuses the financial changes the changes form already refuses. A rule whose path is policy.fullTermBillingInfo or below it returns 422 policy-history-repair-invoicing-enabled, and rules that would change any fullTermPricingInfo value other than a pricing component’s label return 422 policy-history-repair-pricing-financial-change. The dry run refuses as well as the apply, and nothing is written. Other rewrites, including component labels, are unaffected. Invoicing-disabled behavior is unchanged. Kept-invoice credits, invoicing-on companies. A policy invoice plan may now credit a line item that a kept invoice carries but the policy’s stated billing omits — tax already paid on a policy cancelled flat, say, when the cancellation’s billing states no tax line. Such a line conserves at 0, and a create credits it on the same line with a negative amountCents. This applies to Update Policy Invoices, to the invoicePlan on policy transactions and quote binds, and to the plans Check Bind Conditions and Complete evaluate. A policy whose billing states no lines may accept a plan that only credits kept lines. A line neither the billing nor a kept invoice names is still refused with 422 finv2-policy-batch-unpriced-charge, and a 422 finv2-policy-invoice-not-conserved refusal now names each off line instead of citing its id. Invoicing-disabled behavior is unchanged. See Billing & getting paid. Generated invoice plans credit paid excess, invoicing-on companies. When kept invoices bill more of a line than the quote’s billing states, the plan generated by Bind Quote and Check Bind Conditions with generateInvoices now credits the excess that paid or payment-locked invoices carry: one credit invoice per invoice type, with a negative amountCents on each such line, dated at the transaction’s effective date and payable to the reversed invoice’s payee. A cancellation of a paid policy that used to return 422 finv2-payment-generation-refused from bind, or a readiness failure from the check, now binds or reports ready. Unpaid excess is still voided under the chosen handling; excess on an unpaid invoice the handling keeps is still refused, naming the line. Lines below their target still get new installments, and a dollar down payment is now a share of the installments’ receivable lines only. Invoicing-disabled behavior is unchanged. Transaction undo and billing refusal wording, invoicing-on companies. Delete Policy Transaction now refuses with 409 finv2-policy-undo-restates-invoices while the policy has active invoices and the surviving version’s fullTermBillingInfo would give a line a different amount, or no billing at all; nothing is deleted. Void those invoices first, or record a new transaction that changes the billing with an invoice plan. Two messages now name the fix for the door that refused, with unchanged codes and details. finv2-policy-billing-required from Check Bind Conditions, Complete and bind by ID says the quote’s billing calculation produced no billing; the policy transaction API and bind by value keep their text. finv2-policy-billing-checksum from the policy transaction API and Update Policy Invoices says to restate fullTermBillingInfo to match fullTermPricingInfo.total; quote doors keep the re-rate advice, and every message keeps the signed gap. Invoicing-disabled behavior is unchanged. See the billing aggregate. Documentation fix — no behavior change. The Patch Configuration request schema now maps each changes[].kind to its change schema, so clients generated from the specification type each change by its kind again. update-billing-condition-row states its rule, at least one of expression or message, in its description instead of an anyOf that collapsed generated types. Documentation fix — no behavior change. Patch Configuration now documents every change kind it accepts: field, option-set, custom-object and card deletes, update-card, and the add/update/delete kinds for smart tags, export columns, built-in export column types, list columns, list filters and conversion rules. add-card, add-field-to-card and update-field-on-card now list displayCondition, cardTitleExpression, row and column where they accept them, and add-field-to-card no longer marks inputModality required. List filter kindInfo lists Percentage. BordereauColumnSpec and Fmv1ConfigSpreadsheetType now map each kind to a named schema.

October 6, 2026

Staff policy repair. The fieldValues kind accepts generic changes and can include linked bound Quotes. A dry run reports full-history fingerprints, stored diffs and structural rebuild proof. Apply refuses unsafe history and preserves pricing financial values. The legacy rewrites manifest remains supported. Policy invoice batches, invoicing-on companies. Update Policy Invoices now refuses stored receivable billing that disagrees with the policy’s pricing total with 400 finv2-policy-billing-checksum, before any invoice, journal or number writes. Correct the policy’s pricing or billing through a policy transaction before retrying. Missing billing keeps its 422 refusal, and a valid standalone batch may still void all unpaid invoices. Invoicing-disabled behavior is unchanged. Financial readiness, invoicing-on companies. Complete and Check Bind Conditions now enforce the same receivable checksum as Bind across all five quote types, using converted Policy or prepared lifecycle pricing. Billed same-term changes must restate billing before invoice conservation runs. A checksum failure reports the signed price-minus-receivables gap and re-rate guidance; failed completion rolls back every edit in its PATCH. Check also accepts generateInvoices=true to dry-run Bind’s generator without saving the plan, and cancellation/reinstatement return their own billing scopes. Invoicing-disabled behavior is unchanged. See Check Bind Conditions and Billing & getting paid. Quote-owned invoice targets, invoicing-on companies. Invoice generation and quote invoice checks now use the quote’s own billing aggregate, including cancellation and reinstatement, without deriving a policy-carry preview. Missing billing remains distinct from a stated empty aggregate. Invoicing-disabled callers keep their existing behavior. See Billing & getting paid. Invoicing-enabled companies: billing at bind. Cancellation and reinstatement now store the quote’s or caller’s billing without scaling or restoring it. Endorsements must also state billing again on an already-billed term (400 finv2-policy-billing-required). A same-term change cannot void every existing invoice while billing still owes (400 finv2-policy-invoices-required). New terms and unbilled policies may bind without billing; policies without invoices may bind with billing alone. The quote billing readiness gate has been removed from Bind, Complete and Check Bind Conditions. Invoice conservation remains. Both invoicing-disabled modes retain their existing behavior. See Billing Aggregate. Rating freshness has been removed. Rating workflows no longer accept freshness, and Check Bind Conditions no longer includes rating advisories. Ordinary rating preconditions, authored warnings and invoice integrity checks remain in effect.

2026-10-06 (Browser-safe billing calculations)

Tightening, invoicing-on companies only. Configuration import and validate refuse NO_BILLING() as the whole Quote.fullTermBillingInfo calculation and reject server-only or non-pure functions, including calls in calculated fields it depends on. Existing invoicing-off configurations keep their admission behavior. A platform migrating registry, initially empty, lets an authorized company install its real calculation before invoicing is enabled. Its quote saves and rating results keep billing null until the flag is enabled. See Quote billing calculations.

2026-10-06 (Named billing conditions)

Additive, invoicing-on and platform migrating companies only. Configuration accepts optional top-level billingConditions, with a default row per named condition and per-flow overrides. Import validates their boolean expressions and browser-pure calculated dependencies. A false condition skips the Quote billing calculation and returns null on save and rating without itself refusing either saving or binding. Configurations without conditions import and export as before. See Billing conditions.

V1 API Changelog

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

2026-10-08 (endorsement, cancellation and reinstatement quotes belong to their policy)

Breaking change. A quote’s parent now depends on its quoteType. New business and renewal quotes still belong to a submission. Endorsement, cancellation and reinstatement quotes belong to the policy they change, through referencingPolicy, and need no submission.
  • Affected: POST /api/v1/companies/{companyId}/entities/quote and PATCH /api/v1/companies/{companyId}/entities/quote/{entityId}.
  • An endorsement, cancellation or reinstatement quote must name its policy. A create without referencingPolicy, and an update that removes it, are rejected with 400, problem code EntityInvariantViolation and the message “An endorsement, cancellation, or reinstatement quote must reference the policy it amends.” Before, the same request was rejected unless it named a submission.
  • It no longer needs a submission. A create with referencingPolicy and no referencingSubmission now succeeds. Nothing is minted for you. A quote that already has a submission keeps it, and you may still send one.
  • New business and renewal are unchanged. They still require referencingSubmission, with the same 400 as before; a renewal’s source policy is previousPolicy, not its parent.
  • From the submission side, a PATCH /entities/submission/{entityId} whose referencingQuotes drops an endorsement, cancellation or reinstatement quote is no longer rejected with SubmissionUpdateWouldOrphanQuotes while that quote still names its policy.
In the app, starting an endorsement, cancellation or reinstatement no longer creates a submission. The unfinished change appears on its policy’s page.

2026-10-07 (Event export line item columns)

Additive. Event exports offer three financial columns for each live Line Item Type under an Invoice Type: finv2::overview::<categoryId>::lineItem::<lineItemTypeId>::paid (for example “Loss — Deductible — Paid”), …::paidToDate (”— Paid to date”) and …::owed (”— Owed”). Paid reads like the Invoice Type’s Paid: with a startDate on Start Entity Export Run it is what was paid during the period, otherwise everything paid up to asOf. Paid to date and Owed are always as of asOf. A line item’s Paid and Owed add up to its Invoice Type’s. There is no per-line-item Reserves. Saved export presets and exportPlatformColumns accept the new keys. Existing columns, and exports that don’t select the new ones, are unchanged.

2026-10-07 (Event export opening balances)

Additive. Event exports offer two more financial columns for each Loss or Recovery Invoice Type: finv2::overview::<categoryId>::reservesAtPeriodStart (”— Reserves (period start)”) and finv2::overview::<categoryId>::totalAtPeriodStart (”— Expected Total (period start)”). With a startDate on Start Entity Export Run, they hold the balance as of the day before startDate, so an entry dated on startDate counts in the period’s Paid and not in them. Without a startDate they are blank. Saved export presets accept the new keys. Existing columns, and exports that don’t select the new ones, are unchanged.

2026-10-06 (Event export reporting period)

Additive. Start Entity Export Run accepts an optional startDate (YYYY-MM-DD) on Event exports. With it, Paid columns sum the activity effective from startDate through asOf (today when omitted), both dates included, while Reserves, Owed and Expected Total stay balances as of the end date. A startDate after the end, or on another entity type, is a 400. Such a run downloads as event-export-<startDate>_<asOf>-<runId>. Requests without startDate are unchanged.

2026-10-02 (Person email is framework-required)

Tightening. A configuration import, validate or compare body must declare the Person field email as Text with Single cardinality and no calculated value. A body that leaves it out or declares another shape is rejected with FRAMEWORK_REQUIRED_FIELD_INVALID naming Person::email. Every configuration we provision already declares it this way, and stored people are unchanged: a person with no email still reads as null.

2026-10-02 (Sandbox Reset removes broker portal memberships)

Behavior change. POST /api/v1/companies/{companyId}/reset deletes every Person, so it now also removes the company’s broker portal memberships, which link a broker’s login to a Person. The brokers’ users and logins are kept. Go App memberships are unchanged. See Companies.

2026-09-30 (Export columns require spreadsheetType)

Tightening. Every exportSurfaces row in a configuration import, validate or compare body must declare spreadsheetType, and a row carrying valueType or cardinality is rejected with a message naming spreadsheetType. The add-export-surface-row patch change requires spreadsheetType, and neither export-row patch change accepts valueType or cardinality. Configuration exports already carry spreadsheetType on every export row.

2026-09-29 (Configurable card spacing)

Additive. Card configuration accepts optional rowGap and columnGap, nonnegative numbers of extra pixels between grid rows and columns. They are preserved by configuration import/export, card descriptions and page layouts. The add-card and update-card patch changes accept them too. Omitted values preserve existing spacing; 0 removes any extra gap in that direction. See Card spacing.

2026-09-29 (Entity list sorting follows each field’s type)

Tightening. On List Entities, entity export runs and data validation, sortBy now sorts by what a value reads as: numbers and percentages by value (a percentage used to sort as text), text case-insensitively with numbers in natural order (POL-2 before POL-10), dates by day, option sets by option label A to Z, addresses by country then state, and a multi-value field by its first value. Blanks sort last in both directions. sortBy also accepts a declared list column key. An unknown sortBy, or one naming a column that cannot be sorted (linked-record and user fields, calculated list columns, structured values, embedded exposures), is now a 400 InvalidSortBy instead of being silently replaced by the default order; a linked-record field no longer sorts by the linked record’s id.

2026-09-29 (Structured configuration field type inputs)

Additive. Full configuration inputs and the add-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 (Calculated-field failures reject writes)

Stricter validation. A configured calculation that cannot compile or evaluate, or returns a value that violates its field schema, rejects the entire write with HTTP 400 / InvalidFieldModelV1Data. The error identifies the entity, field path and cause. This applies to entity creates and updates, embedded exposures, and Policy transactions; changes made within the failed write are rolled back. Valid null or missing results remain subject to other validation rules, including required-field rules. Validation of values supplied directly in API requests and the rules governing when calculations run are unchanged. This change does not bulk-recalculate existing records. See how calculated fields work, calculation failures and error handling, and Policy transaction validation.

2026-09-28 (Policy validation and repair)

Policy validation includes versionRegressions, 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 with x-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 in customObjects, 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 accept customObjects 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 whole eventOpenCloseHistory 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: warning diagnostic with the code rating-workflow-warning and the configured message, beside the rated data. 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 absent data field, never the mere presence of diagnostics.
  • The polarity matches preconditions: true means satisfied and silent.
The configuration schema for a rating workflow also now documents 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 carry Cache-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 successful POST /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)

Added POST /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)

Added GET /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 support fullTermPricingInfo: 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:
Closing a submission atomically cancels its active quotes and may stamp an optional close reason and explanation. Reopening recomputes the submission’s status from its live quotes. Cancelling a quote moves an unbound 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 one InvalidEntityShape 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 a privileged 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)

Check Bind Conditions 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 a resource.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 be stageType, 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 use documentVersion: 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 use Option 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 the objectPrimitives 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)

Company paymentDefaults 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’s payeeDefaults.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-wide policyFinancials.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 on PATCH /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 screenedAt than 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 screenedAt is 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.
This does not change any schema. It changes which value wins when two disagree.

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’s name 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: null may now carry { "name": "…" }. The field’s type is unchanged; a client reading it as “has this been screened?” should test status, 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 with 400 / InvalidFieldModelV1Data, each refused value named in details;
  • the event lifecycle effectiveOnDate (and every entry of a replaced open/close history) is now an Fmv1Date too, not the bare { year, month, day } triple it was documented as.
Nothing is converted on the caller’s behalf. Integrations that were still sending the retired spelling on 2026-09-08 have been contacted and keep working unchanged until they migrate; a new integration must send the canonical object. List filter literals are unaffected: a filter’s 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}/folders and GET /api/v1/companies/{companyId}/folders/{folderId} return category on every folder node (null when uncategorized).
  • POST /api/v1/companies/{companyId}/folders accepts an optional category.
  • PATCH /api/v1/companies/{companyId}/folders/{folderId} accepts category as a third updatable field — a label to set, or null to clear it. Omit it to leave the folder’s category alone.
An unconfigured label is rejected with 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 on POST /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 with 400 / InvalidDelta, each refused value named in details;
  • a write to a single member of a Date (policy.policyEndDate.year) is refused with 400 / InvalidDelta — a Date is written whole.
Every other write path (entity create and update, new business, renew, cancel, reinstate) is unchanged. Responses have carried only the canonical Date object since 2026-09-03.
  • Documentation fix. The Fmv1Date schema now describes the value the API stores and returns: date (required) and timezone. It previously declared day / month / year required 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. screenedBy on a screening, and reviewedBy on 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 failureCategory values are InvalidRequest, HttpError, NetworkError, Timeout, ProviderError and InvalidResponse; the earlier example showed a lower-case timeout that the API never returns.

2026-09-02 (Google Sheets rating stages are no longer accepted)

Breaking change. Configuration documents no longer accept the retired segment-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-service and a full-term Google Sheets stage with full-term-rating-service.
  • Set raterSpec.args.raterId to 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.
Existing Rating Run Logs and their evaluated Debug Sheet Excel/CSV downloads and Google Drive exports remain available.

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: OfacScreening exposure field carrying the OFAC Screening input modality). A field that is not placed that way is refused with 400 and no provider call is made.
  • Sanctions lists, match threshold and provider are fixed platform settings, recorded on each result. Sending sources or minScore is a 400, not a silently ignored key.
  • A provider failure is a 200 with status: 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/quote and PATCH /api/v1/companies/{companyId}/entities/quote/{entityId}.
  • Create the submission first. POST /entities/submission, then send its id as referencingSubmission on 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 referencingSubmission at another submission is a normal edit. Setting it to null is not.
  • The rule holds from the submission side too. A PATCH /entities/submission/{entityId} whose referencingQuotes drops a quote that no other submission takes on is rejected with 409 and code SubmissionUpdateWouldOrphanQuotes, 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).
No other quote behaviour changes. Rating is unaffected — 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’s category 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} and PATCH /api/v1/companies/{companyId}/files/{fileId}/placements/{placementId}.
  • Matching is case-insensitive, and the stored value is the configured spelling. Sending loss runs against a configured Loss Runs succeeds and stores Loss Runs.
  • Clearing is always allowed: category: null is 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 flagged configured: true are the writable ones.
  • If an owner type has no configured categories, no category can be set on its files until one is added.
Every category label already in use on existing files was adopted into its company’s configured list ahead of this change, so an integration sending labels that were already in use is unaffected.

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/rate and POST /quotes/{quoteId}/rate are 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 policy endorse transaction 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.
Nothing about the endpoints’ behaviour changed; if you already treated rating as a read and persisted results yourself, there is nothing to do.

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/seed takes { program?, replace? }. The starterSheet and modules fields are removed; a request that sends either is rejected by the route validator. Omit program to seed the product default (gl-pl). The error codes UnknownStarterSheet, UnknownStarterModule, and InvalidModuleSelection are replaced by the single UnknownStarterProgram.
  • POST /configuration/seed/generate takes { program? } for the same reason.
  • GET /configuration/seed/options returns { defaultProgram, programs }. The defaultStarterSheet, options, and modules properties — and with them the group / alwaysOn / defaultSelected axis metadata — are removed.
Behaviour change. The seeded starter configuration is a single realistic GL/PL program whose money comes entirely from its rating workflow’s projection stages. Pricing is exactly three components — 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 surviving readOnly field means the same thing: a value you send is silently ignored and the server-managed value wins — no field rejects a supplied value with 400 GeneratedFieldWrite any more. The fields that used to carry the mark are ordinary calculated fields now; read their x-variant / x-purity marks instead.
  • The configuration import no longer accepts a generated property on a field or sub-field. Sending one is rejected with a message pointing at the replacement: recalculate: "WHEN_UNSET" (with clientValueAllowed as needed) for run-once values, termInvariant for 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 PATCH returned 409 (GuardedStatusFieldWrite), and an explicit eventStatus on create was honoured. Both are gone: an integration that relied on the create behaviour to load already-closed claims must now supply the event’s eventOpenCloseHistory instead, from which the platform derives the status and close date.
  • Unchanged: the status still moves through POST /events/{eventId}/close and POST /events/{eventId}/reopen.
  • quoteStatus and submissionStatus are unaffected — they remain guarded and still answer 409 on a disallowed generic move.
Additive. 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 a transactionType: 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, and POST /policies/transaction/renew), POST /quotes/bind, and GET /policies/{policyId}/versions/{version} — and the segments[] of each item returned by GET /policies/list, GET /policies/versions, and GET /policies/{policyId}/versions (those three include segments only with detail=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 reads ENDORSE across its whole span — including the part the endorsement did not edit — while the versions beneath it keep the type that wrote them.
  • null means UNKNOWN, not new business. The segment was written before this member existed. Existing segments are not backfilled, so treat null as “no answer” rather than inferring a type from it.
The member is always present on a segment (required, and nullable). No request shape changes and no action is required.

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).
Unbound quotes are unaffected, as is every other entity type. The MCP 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 only name, 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 5xx now return HTTP 200 with severity: error diagnostics and no data — rating vendor failures (already 200 since 2026-08-25, now also carrying a location), 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 outputPath or missing rater args), an unbuildable InsCipher tax plan — now fail the rate with severity: error diagnostics naming the cause.
  • Per-output-field skips (values a rater produced that could not be written to the quote) now ride beside data as severity: warning diagnostics.
As before, detect failure by the absent 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: warning diagnostics alongside data when a rater reports them. A clean rate’s response is unchanged (no diagnostics key). As before, the failure signal is the absent data field, never the mere presence of diagnostics.
  • Each diagnostic can carry an optional location object naming where in the rating workflow it arose: stageIndex, raterDebugName, an optional targetPath, and the rated segmentWindows (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.
No action required: both changes are additive. (This release shipped the contract; the rater adoption above followed it.)

2026-08-25 (portable Financials Config Document v1)

Added POST /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-derived retirementIdentities 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/rate
  • POST /api/v1/companies/{companyId}/quotes/{quoteId}/rate
A failure during rating execution — for example a rating vendor error, which previously surfaced as a 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 still 400; an unknown quote id is still 404; no configured workflows is still 422.
  • Unexpected application faults are still 500.
  • A successful rate still returns data (with no diagnostics today).
Action required for callers that branch on HTTP status: a rating failure is no longer detectable via a non-2xx status — check for the missing 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 ordered columns grammar), minus limit/offset — a run always exports every matching row. Returns {runId, outcome} immediately; duplicate starts collapse onto the run already in flight (?force=true to 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, and expired once 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), named bordereau-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 supplies folderId and their own googleOAuthToken, and the spreadsheet is created as that user. (Writing into an existing spreadsheet via spreadsheetId, and the custom spreadsheetName, do not carry over — each delivery creates a fresh spreadsheet named bordereau-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, kind or earningBasis you 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) and value (number) remain required; qualifier still defaults to "", earningSchedule (pro-rata | immediate) still defaults to pro-rata, and <classification, qualifier> must be unique within a contract.
  • An absent or null fullTermPricingInfo stays 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: 400 with InvalidPolicyData on new-business / renew, and InvalidFieldModelV1Data on endorse / cancel / reinstate and 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:
  • GET or POST /companies/{companyId}/data/{entity}/validate
  • GET /companies/{companyId}/configuration/data-validation-runs/{runId}/findings
For example, ['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. pricingComponents of kind BrokerCommission or ProgramCommission remain accepted and stored exactly as before; their amounts simply report into no rollup. They were already excluded from total by definition, so total does 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.
The pricing contract’s server-computed, read-only rollups are now 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 no classification — is rejected with a 400 whose 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 carrying fullTermPricingInfo, and POST /quotes/rate.
Dual components stay accepted after the migration: you may keep sending the legacy fields alongside a 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 of earningBasis. Omitted means pro-rata.
Together <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 receivable amounts sum exactly to fullTermPricingInfo.total, refused otherwise with a 400 and the code finv2-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 the direction you send must be the one that line item is configured with — a checked assertion, not an input. Otherwise a 400 with the code finv2-policy-billing-vocabulary, which names every offending line rather than the first.
An aggregate you do not state is exempt from both. “Stated” means lines is present, empty array included — so "lines": [] beside a non-zero price is a checksum refusal, not an exemption.
This is not the old fullTermPolicyBillingInfo returning. That retired name belonged to the container now called fullTermPricingInfo, and it stays rejected. fullTermBillingInfo is a new container holding a genuinely different fact: a price is not an obligation.
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:
It is still a 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.
This is the only reliable signal. The endpoint, the entity type, and the field’s name all stay the same across the migration; only the configured type changes. What moved: Before — a field typed Object: Address:
After — the same field typed Object: AddressV2:
You do not send county, coordinates, or precision. Geocoding runs server-side on save and fills geocode for you, and it never gates the write — a provider miss or outage is recorded as an unmatched or failed geocode, not an error on your request. If you already hold a geocode, send it under geocode with source: "provided" and it is stored as given without a lookup; a geocode sent with any other source is discarded and replaced by the platform’s own result. Reads. Entity reads — fetching one entity, and the entity list endpoints — return a field configured AddressV2 in the AddressV2 shape, including rows that were written before the field was migrated, so you never have to handle both shapes on one field. Reading county moves from the top level to geocode.county, and geocode.granularity tells you how precisely the coordinates (and therefore that county) were located. zipCode is still a string, and still the most common failure. The path is now enteredAddress.zipCode. JSON numbers cannot represent leading zeros, so 02140 parses as 2140; always quote it. Fields that have not been migrated are untouched. Address stays fully supported and is not deprecated — every field still typed Object: Address behaves exactly as before. Full reference: AddressV2 and the Fmv1AddressV2 OpenAPI schema.

2026-08-14 (fullTermPolicyInfo is removed)

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

fullTermPolicyInfo no longer appears on any policy response, in any persisted policy segment we write, or in the framework configuration. The deprecation notice in the 2026-08-13 entry below announced this; the container was a platform-derived mirror, and once every value it carried had a root field there was nothing left for it to say. Who this affects. Any integration still reading a value out of fullTermPolicyInfo. Writers are unaffected — the container has been read-only and platform-derived since the term-bound cutover, so nobody sends it, and a payload that still includes one is simply ignored. Where each member went:
  • fullTermPolicyInfo.policyNumber → root policyNumber
  • fullTermPolicyInfo.policyStartDate → root policyStartDate
  • fullTermPolicyInfo.policyEndDate → root policyEndDate
  • fullTermPolicyInfo.previousPolicyId → the relationships surface. This is the one member with no root field, and the change the 2026-08-13 entry said would not happen before it had one. It has a better home instead: renewal lineage is the policy’s own previousPolicy link, which you already send on POST /transaction/renew and which the platform writes as the Policy<N:1:previousPolicy>Policy relationship. Read the link back from the policy’s previousPolicy field on any segment, or walk the chain from the expiring side. The container’s copy was a display mirror of exactly that link and could never say anything the link does not.
  • fullTermPolicyInfo.primaryInsuredName / .primaryInsuredJoin → root primaryInsuredName / primaryInsuredId. These left the container on 2026-08-10; see that entry.
Each root field holds exactly the value its container member held, so every move except previousPolicyId is a one-line change per read with no behavioural difference. Historical data is untouched. Policy segments and quotes written before this change still carry the key in their stored JSON, and we neither rewrite nor hide it there. What changed is that nothing on our side reads it and no response carries it. Configuration. fullTermPolicyInfo and the FullTermPolicyInfo custom object are no longer framework-required rows, so a new company is provisioned without them. An existing configuration that still declares them keeps importing cleanly; the rows are ordinary tenant rows now, read by nothing.

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

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

Every response that carried the policy number and the term bounds only inside fullTermPolicyInfo now also carries them at the root:
  • GET /policies/list, GET /policies/versions, GET /policies/{policyId}/versions — on each item’s summary.
  • GET /policies/{policyId}/versions/{version}.
  • The five policy transactions — new business, renew, endorse, cancel, reinstate.
policyNumber is a string. policyStartDate and policyEndDate are the structured Date object ({day, month, year, timezone}) — the same value the container holds, not the ISO startDate / endDate fields beside them, which report the span the version covers and shrink when a policy is cancelled. All three are null only when the version carries no policy data. Nothing was removed or renamed, so no integration breaks on this change. The values are read from the same cross-segment-validated source the container is, so a root field and its container copy can never disagree on one response.

Deprecated: fullTermPolicyInfo — read the root fields instead

fullTermPolicyInfo is deprecated on every policy response that carries it. It is still returned, unchanged, during the transition window; it will be removed once readers have moved off it (design of record: ADR 0043 — term invariance is a per-field configuration property, and the container is deleted). Who this affects. Any integration that reads a value out of fullTermPolicyInfo. Writers are unaffected: the container has been read-only and platform-derived since the term-bound cutover, so nobody sends it. What you see now. No change — the container is still present and identical. The OpenAPI spec and the generated TypeScript client now mark it deprecated, so a typed client will surface a deprecation hint at the call site. What to do — move each read to its root field:
  • fullTermPolicyInfo.policyNumber → root policyNumber
  • fullTermPolicyInfo.policyStartDate → root policyStartDate
  • fullTermPolicyInfo.policyEndDate → root policyEndDate
  • fullTermPolicyInfo.primaryInsuredName / .primaryInsuredJoin → root primaryInsuredName / primaryInsuredId (already removed from the container; see the 2026-08-10 entry below)
  • fullTermPolicyInfo.previousPolicyId — not hoisted yet. Keep reading it here; it is the one member without a root field, and the container will not be removed before it has one.
Each root field holds exactly the value its container member holds, so the move is a one-line change per read with no behavioural difference.

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

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

fullTermPolicyInfo is platform-derived — every persisted write replaces it with exactly its four framework members, so nothing you send inside it is ever read. This reference has said as much since the cutover (“anything else you put in it is discarded”), but the write path was rejecting an undeclared member instead of discarding it:
That fell hardest on callers of the entry below. The two retired primary-insured members were deliberately left in already-stored policy segments and quotes, so a client that read a policy, changed one field, and wrote the record back was handed the retired keys by our read and then refused by our write. An undeclared member of fullTermPolicyInfo is now dropped from the payload before validation, on every write path. Nothing else moves: the four declared members are still shape-checked, a term stated only inside the container is still rejected (the term comes from the root policyStartDate / policyEndDate), and the other containers — fullTermPricingInfo, fullTermPolicyRatingResult, crossSegmentRatingOutputs — still reject an undeclared member, because those carry facts you own rather than a mirror of ours.

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

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

Breaking: fullTermPolicyInfo.primaryInsuredJoin and fullTermPolicyInfo.primaryInsuredName are removed

Both members were dropped from every company’s configuration. Sending either one returned 400 InvalidFieldModelV1Data — Unknown field 'fullTermPolicyInfo.primaryInsuredJoin' — from 2026-08-10 until the fix in the entry above; from now on they are accepted and discarded, but they are still gone from the contract and nothing reads them. fullTermPolicyInfo is term-constant, so a primary insured recorded there was frozen for the whole term — and a primary insured genuinely changes mid-term. The container is now closed at four members (policyNumber, policyStartDate, policyEndDate, previousPolicyId), and the primary insured is a per-segment fact you can endorse like any other field. What to do:
  • Remove both keys from your payload. Nothing replaces them on the request.
  • The primary insured is derived, not sent — it comes from the policy’s own exposures, by the rule your configuration defines (typically the exposure your config marks as primary). Set the exposure; the platform resolves the insured.
  • Read it back from the root primaryInsuredName / primaryInsuredId fields on the response — hoisted at the top level and present per segment. They resolve per segment, so a mid-term endorsement of the insured now takes effect from its endorsement date; the flat response fields show the last segment’s insured.
Values already stored under the two removed keys were left in place rather than migrated. Any policy you write again has its container replaced wholesale, which clears the residue on that write.

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

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

All five policy transaction endpoints now accept optional invoicePlan, an explicit keep/void/create plan that commits atomically with the new policy version. Omitting it preserves ordinary transaction behavior and creates no invoices. When a pricing restatement touches a policy with active invoices, the plan is required and must conserve the new pricing contract. The same contract is available directly at POST /api/v1/companies/{companyId}/financials/policies/{policyId}/invoices/batch. Existing invoices omitted from voidInvoices are kept; every void must cite its current headJournalId; dates, payees, and creates are never inferred. Its body is exactly that plan, with no author field, and a company whose separate policy-financials rollout gate is off receives 403 rather than 404. Quote binding also accepts { "generateInvoices": true }. The server resolves policy-invoice presets into that explicit plan before binding; omitting the flag keeps bind behavior unchanged. Presets requiring a broker payee fail closed until a canonical broker party is available. Ad-hoc create or void requests against policy-linked invoice documents continue to return 422 POLICY_INVOICE_BATCH_ONLY, now with the standalone batch route as the recovery path.

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

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

An event (a claim or an incident) now closes and re-opens through dedicated endpoints that take the date the action takes effect:
Each moves eventStatus, stamps or clears the event’s close date, and appends an entry to its open/close-history log — in one transaction. Both require company.claim:create and return { eventId, eventStatus }.

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

A generic update that changes eventStatus is now rejected with 409 (GuardedStatusFieldWrite), and the message names the endpoint to use instead. The lifecycle dates a claim reports — opened on, previously closed on, re-opened on — are derived from the open/close-history log, so a status change that skipped the log would silently misreport them. Two things are deliberately unaffected: setting eventStatus on create still works, so a historical import can load claims that were already closed; and an update that sends eventStatus with the value it already holds is still accepted, so a client that echoes a whole record does not break. If you close events through the API today, move those calls to the new endpoints. The x-computed-default mark on eventStatus is unchanged and remains accurate for creates — the new Flow-written status fields section explains the distinction.

Added: 409 EventLifecycleDateOutOfOrder

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

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

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

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

Removed: the old key is no longer accepted in requests

The fullTermPolicyBillingInfo key — accepted (deprecated) during the transition window on NEW_BUSINESS/RENEW payloads and the ENDORSE/CANCEL/REINSTATE channels — is now rejected like any other unknown key/field. Stored data and every tenant configuration were migrated to the new vocabulary, so reads never produce the old name either. What changes for you. Nothing, if you already moved to fullTermPricingInfo during the transition window.
  • Reading. Read the container as fullTermPricingInfo everywhere the old key used to appear (version/transaction responses, the policy list summary, saved views, exports). Read totals from the five rollups; itemized charges from pricingComponents. Bordereau column ids are unchanged (policyPremium, policyPremiumChange) — only configured field-column paths move (e.g. fullTermPricingInfo.taxes).
  • Writing. Send pricingComponents under fullTermPricingInfo (one component per charge, classified by kind) and let the platform compute the rollups. A payload naming fullTermPolicyBillingInfo now fails with a 400 (unknown key on the transaction channels; undeclared field inside fieldModelV1Data.policy).

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

Breaking: policy transactions send and return the field data unwrapped

The fieldModelV1Data.policy wrapper is gone from the policy-transaction request and from every policy response. There is no accept-both window: send and read the new shape. Requests — POST .../policies/transaction/new-business and POST .../policies/transaction/renew now take the policy field data as data, a plain object, with the command metadata beside it:
Both endpoints accept the same three keys — transactionTimestamp, displayAuthor, data. (displayAuthor is new on renew; it already existed on new-business.) Responses — every policy read and write returns each segment as { startDate, endDate, data }. The old spelling was { startDate, endDate, fieldModelV1Data: { policy: {…} } } on the single, versions, transactions and bordereau reads, while the list read already returned the unwrapped object under the fieldModelV1Data name. Both are now data, so the two read paths agree for the first time. This covers the list, single, versions, transactions and bordereau reads and the MCP policy tools. The three hoisted whole-term containers on a version response — fullTermPricingInfo, fullTermPolicyInfo, fullTermPolicyRatingResult — are unchanged, as are the hoisted primaryInsuredName / primaryInsuredId. Entity CRUD is untouched: a Quote, Exposure or Event still carries fieldModelV1Data. Only the policy surface changed, because only there did the name wrap a second object. Error messages no longer name the wrapper. A payload-shape failure on these two endpoints now returns error code InvalidPolicyData (was InvalidFieldModelV1Data) and names the field without a policy. prefix — for example 'policyStartDate' is required (Date object). The generic InvalidFieldModelV1Data code is unchanged for entity CRUD.

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

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

Deleting a form now returns 409 with error code form-template-referenced-by-form-logic when the live field-model configuration still has a forms-logic rule for that form number. Remove the rule through the configuration import flow, then retry the delete. This guard also applies to deletion from the app’s form library. It prevents a form cleanup from leaving an invalid configuration that blocks a later config import or config-sync reconciliation. Forms with no live forms-logic reference continue to delete as before, and already-generated forms remain readable.

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

Changed: a generated field publishes readOnly + x-generated

Some fields are generated by the platform: a sequence number minted when a policy binds, an identifier composed from other fields. Originating or changing one has always been rejected with a 400 GeneratedFieldWrite. The /configuration schema did not say so — a generated field could appear with no readOnly mark, and one expressed as a keep-if-supplied calculation was published as a computed default, whose documented meaning is “a value you supply is kept”. Both told you to send a field the API refuses. Such fields now publish readOnly: true + a new x-generated: true mark, with a description saying the value is generated and a supplied one is rejected. The new Generated write tier documents it, and the MCP get_entity_schema tool surfaces it as a generated boolean. No rejection behavior changed — a value the platform did not originate was refused before and after, and resending the exact stored value is still tolerated on an update. If a create or update of yours was failing with GeneratedFieldWrite on a field the schema said was yours to set, this is why. Drop the field from the payload and read the value back from the response. One quiet fix rides along: an explicit null for a generated field inside an embedded exposure used to blank the value on the embedded copy. It is now ignored, so the platform’s value survives. Generated fields are a per-company configuration choice, so which fields carry the mark depends on your configuration; read them from /configuration rather than hard-coding a list. Fields inside an embedded custom object (a sub-field) do not carry write-tier marks at all yet — that surface is unchanged. An embedded exposure does carry them, in both of its modes.

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

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

A bill review checks one invoice against the company’s own bill review rules and reports what it thinks is wrong with it. Both halves are now on the API:
  • POST …/bill-review starts one and answers 202 with the runId to poll. Nothing is reviewed inside the request — the review is background work. There is no request body, and no actionId or If-Match: the rules are the company’s own (snapshotted when the run is created) and the invoice version is pinned from its current head, so there is nothing for a caller to send, and a review records a verdict beside the invoice rather than journaling an action against it. Requires company.payment:update.
  • GET …/bill-review returns the latest run — status, the passed verdict, error, the pinned invoiceHeadJournalId, and the findings. Requires company.payment:read.
Polling. status is queued/running until terminal, then succeeded or failed. passed is null until then — and stays null forever on a failed run, so read error there rather than reading an empty findings list as a pass. Re-posting is safe. While a review of the invoice is queued or running, a second POST creates nothing and answers 202 with outcome: in_flight and the id of the run already under way. Once that run is terminal the next call mints a fresh one. A company at its background-work capacity gets a 429 and nothing is queued. Staleness is yours to check. invoiceHeadJournalId is the invoice version the run judged; a review is never re-run because the document changed. Compare it with the invoice’s current headJournalId — if they differ, the findings describe an earlier version. Not offered: writing the rules, dismissing a finding, or reading earlier runs. Rules and dismissals are managed in the app, and only the latest run per invoice is exposed.

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

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

An embedded exposure is a point-in-time copy of an exposure, taken once when it is embedded and never re-copied. It now carries only the exposure’s own values. Its relationship fields — contacts, exposureAssignee, referencingQuotes, referencingPolicies, referencingSubmissions, referencingEvents, and any other field whose type is a join — are no longer part of that object, in either the reference mode or the create mode, and no longer appear in the published schema. They were never usable there. A copy cannot hold a relationship: the relationship belongs to the exposure record itself, so the platform read these keys back as null whatever you sent. Removing them makes the document match what actually happens instead of advertising a field that silently did nothing. What changes for you. Nothing, if you were not sending these keys.
  • Reading. Read an exposure’s relationships from the exposure itself (GET /exposures/{id}), not from a host’s embedded copy. This is also the value you want: the exposure’s current relationships rather than whoever was linked on the day the copy was taken.
  • Writing. A relationship you send inside an embedded item is ignored rather than stored on the copy. To change a relationship, write the exposure (PATCH /exposures/{id}). When you create an exposure inline inside a host, a relationship you supply is still applied to the new exposure record — that path creates a real exposure, so the value lands on it and reads back from it.
Value fields are unaffected: you can still override any of them on an embedded copy, and the override still never writes back to the exposure record.

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

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

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

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

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

A data validation run’s results — the status counts and the findings — are now documented as retrievable for at least 30 days after the run completes (finishedAt). This is a floor, not an expiry: results may be kept longer, so do not compute an expiry date from it. Once a run has aged past that floor, both GETs answer 410 Gone instead of 404. The distinction is worth handling: a 404 means the run id is wrong (check it), while a 410 means the id is right and the answer is simply no longer available (start a new run) — so a client holding a good run id is not sent off to debug the id. The 410 body carries the run’s finishedAt and the retentionFloorDays it fell outside of, neither of which a 404 ever carries. Three properties to build against. The two endpoints age out together, so a run is never readable through one and gone through the other. A run id belonging to another company is still a 404, never a 410, so this response can never confirm that an id exists elsewhere. And a run that has not finished is never aged out, however long ago it was started.

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

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

The per-record detail behind a data validation run (permission company.configuration:export), completing the three-endpoint set below. It returns the standard { items, totalCount } envelope, one item per stored record the run judged not to adhere: entityType, entityId, the undeclared extraKeys it holds, and the violations whose values would fail a write. Read a violation’s fieldPath — ordered, machine-stable segments — rather than splitting the legacy dotted field; both are [] / null for a record-level failure that names no single field. It is paginated, and separate from the poll, on purpose. A status poll is made roughly once a second, and a run over a broken book can produce tens of thousands of findings, so the poll stays fixed-size and the findings are pulled here with page / pageSize (default 50, maximum 500 — a larger pageSize is clamped, not rejected). totalCount ignores the pagination, but it is only stable once the run has reached a terminal status (succeeded or failed). A run still queued or running appends findings as it scans, so page 1 of a live run can report a smaller totalCount than page 2. Findings only ever append, never reorder, so the ordering of what you have already read stays put. To size a whole pull from the first page, poll GET /configuration/data-validation-runs/{runId} until the status is terminal, then read the findings. entityType is how you group. The response is a flat list — request one entity type at a time to build a per-type view. The filter narrows totalCount as well, so the count always describes the items beside it. An entityType the run found no problem in is a legitimate empty page; an entityType that is not a real entity type is a 400, not an empty page, because ?entityType=Policies answering {"items":[],"totalCount":0} would read as “none of my Policy records have a problem”. One field is worth calling out: adheres is always false on a returned finding — a row exists only for a record that did not adhere.

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

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

Two endpoints (permission company.configuration:export) that check whether the records a company has already stored would still fit a configuration — the data-aware companion to validate, which reads no records at all. The start endpoint’s request body is optional, and that is how you choose what to scan: send a complete configuration body to scan that candidate without importing it, or send no body at all to scan the company’s live configuration. It returns { runId, outcome } immediately; the scan runs in the background and GET .../{runId} reports status (queued → running → succeeded / failed) plus running counts of records scanned, adhering, holding undeclared keys, and breaking. There is deliberately no total or percentage, and per-record detail is not part of the response. Two behaviours to build against. Starting a run whose configuration matches one already in flight returns that run’s id with outcome: "duplicate" rather than starting a second scan — a successful 200, not an error, so a retrying client should poll the id it gets back; pass ?force=true to start a new run anyway. And a company may have 3 runs in flight at once: a 4th distinct configuration is refused with a 429 whose body carries inFlightRunIds. ?force=true does not lift that limit.

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

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

The 2026-07-24 embedded-exposures write-tier entry below listed these three Exposure fields as system-owned, and a later change began enforcing that on write: the value was silently dropped from an embedded-exposure item instead of stored. That was wrong, and it is reverted. All three are back to settable, unmarked in the published schema, and stored as sent. The tier was a mistake because the platform does not persist these values itself. Hosted rating is stateless — it computes and returns results without saving them — so the payload that writes them back is their only source. Marking them read-only removed the sole copy and left the exposure’s rating fields null. No action is needed if you never stopped sending them. If you removed them from your payloads after that entry, resume sending them and re-save any quote whose exposure-level rating values are now empty. The documented external-rating flow — PATCH the quote with policyRatingResponse and exposureRatingResponse populated — is correct and supported. Whether a future release moves rating persistence onto the platform is an open design question; it will get its own entry, with notice, if the contract changes.

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

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

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

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

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

The record-payment request no longer takes a document-level amountCents. The field is removed: sending it is now a 400. Every request must instead carry exactly one of two modes — both together, or neither, is a 400:
  • payBalanceDue: true — settle the whole document. The server marks every open line item at its full remaining, in the document’s line-item order, in both directions at once. The invoice comes back paid with balanceDueCents: 0, and the gesture’s net cash equals the balanceDueCents you asked to pay. Only the literal true is accepted; payBalanceDue: false is a 400.
  • allocations — an array of { lineItemId, amountCents }, naming exactly the line items to mark and exactly the cents for each. A line you do not name is untouched; a line you name for less than its remaining stays open.
allocations[].amountCents is in the line’s frame — it settles that line’s remaining toward zero, so it carries that remaining’s sign. This is not the oriented net-cash frame balanceDueCents reads in: a receivable-direction line with 3,000 remaining takes a mark of +3000, even though the cash moves in. Each amount must be a nonzero integer, the array must be non-empty, and a lineItemId may appear at most once per request (each a 400). The 422 guards are unchanged and now apply to the marks you author: UNKNOWN_ID for a line item not on the document, INVALID_AMOUNT for a mark opposing its line’s remaining (or a payBalanceDue against a document with nothing open), and AMOUNT_EXCEEDS_BALANCE_DUE for a mark overshooting its line’s remaining. The bounds are per line, not per document — an allocation well inside the document’s balance due is still rejected if it overshoots the line it names. Everything else on the endpoint is untouched: If-Match, the actionId idempotency key, the auto-approval behaviour, erodeReserves, and the response shape (paymentIds + createdPayments, now in mark order — the allocations order, or line-item order under payBalanceDue). Reusing an actionId with different allocations is 409 ACTION_ID_REUSED. To migrate. A call that settled a whole invoice becomes payBalanceDue: true. A call that paid part of one becomes an allocations array naming the lines — read the invoice’s lineItems and its live payments to see what each line has remaining. There is no transition period: the old field stops being accepted with this change.

2026-07-27 (legacy financials endpoints removed)

Removed: the deprecated legacy financials endpoints

Every company now runs on the current financials surface, so the deprecated legacy endpoints are gone from the API:
  • GET /financials/invoice-drafts/{invoiceId} and POST /financials/invoice-drafts/{invoiceId}/finalize → a draft is an ordinary invoice with stage: "draft"; use GET /financials/invoices/{invoiceId} and POST /financials/invoices/{invoiceId}/finalize.
  • POST /financials/event-transactions/import → use POST /financials/events/{eventId}/import.
  • GET /financials/invoice-types → use GET /financials/config/categories.
  • POST /financials/payees/reassign → use POST /financials/payees/{payeeId}/merge.
Each already answered 404 Not Found for every company, so no working integration changes.

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

Added: entityType on List File Categories

GET /files/categories accepts an optional entityType query parameter (the lowercase kebab owner slug: company, event, exposure, quote, policy, submission, person, organization). With it, the response adds the requested entityType and an entityTypeCategories array of { name, configured } — that entity type’s admin-configured categories in their configured order, followed by the labels in use on its live placements that no configured category covers, and categories carries the same names in the same order. Admins manage these lists in Company Settings → File Categories; each entity type has its own. Without the parameter the response is unchanged: categories alone, the distinct values in use across the whole company. Category writes are still free text — Update File and Update File Placement accept any label up to 255 characters and are not validated against the configured lists.

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

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

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

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

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

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

Changed: both bulk wipes now consult the company delete guard

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

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

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

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

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

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

2026-07-24 (embedded exposures)

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

The /entities/{entityType}/configuration schema now renders an embedded-exposure field (an exposure embedded in a Quote, Policy, or Submission) as a oneOf of two modes: reference an existing exposure by id (with optional per-field overrides) or create a new exposure inline (no id). Providing the field restates the complete membership; each item is validated as-if-inserted against the current Exposure configuration, with structured 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.
This no longer holds for eventStatus. Since 2026-08-27 it is system managed: a value sent on create or update is ignored, and the status follows the event’s open/close-history log — 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)

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-supplied actionId idempotency, the If-Match concurrency watermark, stable 422 precondition codes, cursor pagination, integer-cent amounts). Companies are enabled progressively — until a company’s cutover, these endpoints return 404 for it and the legacy financials endpoints keep serving it. Nine single-invoice writes (company.payment:update):
  • POST /api/v1/companies/{companyId}/financials/invoices — create an invoice; the server mints the id and invoice number; optional links (event or policy, plus payee). Replaces the legacy create contract at this path for enabled companies.
  • PUT /api/v1/companies/{companyId}/financials/invoices/{invoiceId} — full document replacement (links excluded).
  • DELETE /api/v1/companies/{companyId}/financials/invoices/{invoiceId} — terminal delete; live payments swept in the same action.
  • POST …/invoices/{invoiceId}/relink — attach/detach/move the event/policy link (zero ledger rows).
  • POST …/invoices/{invoiceId}/payee — set/clear/change the payee.
  • POST …/invoices/{invoiceId}/payments — record a payment; the server mints paymentId and computes allocations pro-rata.
  • DELETE …/invoices/{invoiceId}/payments/{paymentId} — remove a payment; the horizon rule’s companion reserve unwind composes automatically.
  • POST …/invoices/{invoiceId}/void and POST …/invoices/{invoiceId}/restore — write the charges down to zero and back.
Two reserve writes and two compositions:
  • PUT …/events/{eventId}/reserves/{categoryId} — set the scope’s absolute expected total.
  • POST …/events/{eventId}/reserves/{categoryId}/history-reset — zero the scope’s remaining expectation and mark the reserve feed’s horizon.
  • POST …/events/{eventId}/import — wholesale event refresh in one transaction (sweep → create → set reserves), idempotent by client-authored action ids.
  • POST …/payees/{payeeId}/merge — repoint every linked invoice at another payee, re-recording payments; idempotent by convergence.

Changed: POST /financials/validation/sync contract

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

Deprecated: legacy financials endpoints

Deprecated, still serving companies not yet on the new surface; they will be removed after the migration completes:
  • POST /financials/event-transactions/import → use POST /financials/events/{eventId}/import.
  • GET /financials/invoice-types → use GET /financials/config/categories.
  • POST /financials/payees/reassign → use POST /financials/payees/{payeeId}/merge.

2026-07-21

Added: Financials read endpoints

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

Changed: GET /financials/validation/check contract

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

Added: displayAuthor on policy transactions

Policy transactions now support an optional user-visible author label, mirroring the existing displayAuthor on notes and file uploads. When an integration or importer supplies it, the policy history displays the label (e.g. “Data Import”) in place of the acting user’s name; audit attribution (createdBy) stays server-set and unchanged.
  • POST /api/v1/companies/{companyId}/policies/transaction/new-business and POST /api/v1/companies/{companyId}/policies/{policyId}/transaction/endorse accept an optional displayAuthor string (trimmed, non-empty, ≤ 255 chars).
  • PATCH /api/v1/companies/{companyId}/policies/{policyId}/transactions/{transactionId} (new endpoint) sets the label on an existing transaction — display metadata only; no other transaction field can be modified. Requires policy:update.
  • Transaction read responses (get/list) now include displayAuthor (null unless a label was supplied).

2026-07-17

Added: Rate a saved quote by id (stateless)

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

Clarified: Hosted rating is stateless

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

Changed: Legacy smart-tag names are no longer accepted

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

2026-07-16

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

POST /api/v1/companies/{companyId}/quotes/{quoteId}/bind binds a quote into a policy in a single, atomic call — the one-call convenience over hand-rolling the equivalent policy transaction from the quote’s data. There is no request body: the quote is read server-side, and on success the quote is linked to the resulting policy and the policy transaction is committed together, so a quote can never end up bound to a policy that was not created (or vice versa).
  • All five quote types bind through this one endpoint. The endpoint dispatches on the quote’s type: newBusiness and renewal mint a brand-new policy (a renewal’s new policy is linked back to the expiring term), while endorsement, cancellation, and reinstatement transact against their existing source policy in place. The response policyId is the policy the transaction landed on — the new policy for newBusiness / renewal, the existing source policy for the in-place types.
  • Response: 201 { policyId }.
  • Already bound: a quote that is already bound is rejected with 409, and the error body carries referencingPolicy (the id of the policy it is already bound to) so you can recover it without another call.
  • Validation: the quote must satisfy the same policy-create rules a new-business transaction enforces — notably the primary-insured identity on fullTermPolicyInfo (primaryInsuredJoin, referencing an existing Exposure, plus primaryInsuredName). A quote that fails them returns a structured 400.
  • Required permission: company.policy:create — a bind is a policy create.

2026-07-15

Changed: Stateless Quote Rating hydrates exposure id references

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

2026-07-14

Added: Stateless Quote Rating

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

2026-07-13

Added: Touch Form Template and Touch Generated Form

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

Added: List Generated Forms

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

2026-07-10

Added: Back-datable displayDate on Notes and Files

Historical imports and integrations can now supply an optional displayDate (ISO 8601 timestamp) when creating records, so imported history displays and sorts under its original date instead of the import time. Audit timestamps (createdAt/updatedAt) and creator/uploader attribution remain server-set and cannot be overridden.
  • POST /api/v1/companies/{companyId}/notes accepts an optional displayDate; note responses expose it (null when unset). GET …/notes now orders by the display date (displayDate when set, else createdAt), so backdated notes interleave correctly.
  • POST /api/v1/companies/{companyId}/files (upload intent) accepts an optional displayDate; file list items and file metadata expose it. GET …/files now orders by it the same way.

2026-07-09

Added: Explicit modules selection and replace guard on Seed Configuration

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

2026-07-07

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

The legacy additionalColumns parameter is removed from all three bordereau endpoints (GET /api/v1/companies/{companyId}/policies/bordereau, GET …/bordereau/download, POST …/bordereau/export). Field columns are now requested exclusively via columns {"kind":"field"} entries (each a path into the policy’s field data plus a display header); the per-row resolution semantics are unchanged.
  • Migration: replace each {"path":"…","columnHeader":"…"} entry with a columns entry {"kind":"field","path":"…","header":"…"} (on the rendered surfaces, list the fixed columns you want alongside them — columns is the complete ordered set there). A request still sending the removed parameter is not rejected: unknown parameters are ignored, so its extra columns silently no longer appear in the output.
  • The JSON list endpoint now accepts columns, restricted to {"kind":"field"} entries. Column order/omission is meaningless in its typed JSON rows (every fixed property is always present), but field selection is not — the selected fields resolve into each row’s map. A {"kind":"fixed"} entry there is rejected with HTTP 400 and a clear message. (This supersedes the initial columns design note that the list endpoint took no columns at all.)
  • Response rename: the per-row output map on the list endpoint is renamed additionalColumns → fieldColumns (same shape: resolved values keyed by the requested headers; empty object when no field columns are requested).
  • The columns-vs-legacy mutual-exclusion 400 is gone — with one mechanism left there is nothing to be exclusive with.
This ships without a deprecation window: the API has no live third-party consumers yet, so the surface was unified before first external adoption.

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

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

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

The bordereau endpoints (GET /api/v1/companies/{companyId}/policies/bordereau, GET …/bordereau/download, POST …/bordereau/export) now accept any dot-path in a field column — previously a path had to begin with one of the policy-root FullTerm containers (fullTermPolicyInfo., fullTermPolicyBillingInfo., fullTermPolicyRatingResult.) and anything else was rejected with HTTP 400.
  • Per-row resolution moved to the transaction’s effective date. Each row now resolves its field-column paths against the policy data as of that transaction’s effective date, so per-segment fields (e.g. policyStatus, or a mid-term endorsement’s changed values) report the value the transaction put in force. Existing FullTerm and fixed columns are unchanged — their values are identical across the policy term by construction.
  • Malformed dot-paths (empty, or leading/trailing/doubled dots) are still rejected with HTTP 400; a well-formed path that doesn’t exist in the policy data yields an empty value instead of an error.
  • Object-typed leaves render as values, not [object Object] — Date, Address, and Currency fields are formatted; other objects serialize as JSON.

2026-07-02

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

Two new read endpoints complete CRUD on the Forms API, both requiring the forms:read permission:
  • GET /api/v1/companies/{companyId}/forms — list a company’s form library (one summary per form’s current version), paginated (page/pageSize, with a totalCount) and optionally filtered by category and/or kind. Each item includes number, name, kind (template vs static), category, templateKey, version, and timestamps.
  • GET /api/v1/companies/{companyId}/forms/{number} — download a form’s current-version original file (the DOCX for templates, the PDF for static forms) by its FM-XXXX number. Returns { downloadUrl, fileName, contentType, expiresAt }, where downloadUrl is a 15-minute signed URL the consumer GETs the bytes from directly.
Together these enable round-tripping (export a form, edit it, re-upload via the replace endpoints) and config-as-code backup of a company’s form library. The change is purely additive — no existing endpoint changes.

2026-07-01

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

GET /api/v1/companies/{companyId}/files/parse-runs now reports one record per logical parse run — the extract and create_<flow>_v<N> stages of a single trigger, grouped — instead of one flat record per pipeline task. All existing fields keep their types and documented semantics; the change is additive:
  • target is now populated with the { kind, id } file or folder the run parsed (previously always null). Runs that predate grouping still report target: null and are tagged legacy: true.
  • New optional fields per record: stages (per-stage status — including the distinct retrying and pending — plus attempts, error, finishedAt), createdEntities (entityType / entityId / deleted for every entity the run created), extractionReady, rerunOfRunId, reusedExtractOfRunId, and legacy.
  • status is unchanged: still exactly running / succeeded / failed, with every internal retry state reading as failed (per-stage retrying detail now lives in stages).

Breaking: Upload Form Template moved to /forms/template

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

Added: Upload Static Form (PDF)

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

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

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

Added: Delete Form

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

2026-06-30

Breaking: Finalize Upload no longer accepts flow / parseVersionOverride

The opt-in parse trigger has been removed from Finalize Upload (POST /api/v1/companies/{companyId}/files/{fileId}/finalize). The endpoint is now a pure pending → ready flip and accepts only versionId; the flow and parseVersionOverride body fields are gone.
  • Parsing is now reachable only through Trigger Parse (POST /api/v1/companies/{companyId}/files/trigger-parse). To parse on upload, finalize the file and then POST it to trigger-parse.

Added: List Parse Runs

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

2026-06-29

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

Every endpoint has moved off the /api/v1/external/ prefix onto /api/v1/ — the external URL segment is gone, and the v1 version segment is unchanged. There are no backwards-compatible aliases: requests to the old /api/v1/external/* paths now return 404. Consumers must update their base path.
  • The base origin is unchanged (https://go.aiinsurance.io). Only the path prefix changed.
  • Example: POST /api/v1/external/companies/{companyId}/files → POST /api/v1/companies/{companyId}/files.
  • Likewise GET /api/v1/external/me/companies → GET /api/v1/me/companies, and so on for every route.
  • Request/response shapes, 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-templates uploads a .docx form template into a company instance under a chosen category (event, quote-flow, or quote-bind-flow). The DOCX’s embedded smart tags are validated against the catalog for that category before the template is committed: any unknown, not-enabled, wrong-category, or unsupported tag rejects the whole upload with a 400 whose details name each offending tag. DOCX only; the file is sent as base64. Built to support migrating forms between instances with their smart-tag links intact. Required permission: forms:create.

2026-06-26

Breaking: dropped the -json suffix from the configuration endpoints

Now that the spreadsheet configuration endpoints are gone and JSON is the only machine format, the -json suffix is redundant. The four endpoints have been renamed to their clean paths, and their 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 (operationId importFmv1ConfigurationJson → importFmv1Configuration)
  • POST /configuration/export-json → POST /configuration/export (operationId exportFmv1ConfigurationJson → exportFmv1Configuration)
  • POST /configuration/compare-json → POST /configuration/compare (operationId compareFmv1ConfigurationJson → compareFmv1Configuration)
  • POST /configuration/validate-json → POST /configuration/validate (operationId validateFmv1ConfigurationJson → validateFmv1Configuration)
The request/response shapes, permissions, and behavior are unchanged — only the paths and 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. Use POST /configuration/import-json (apply a structured JSON configuration body) for the Google-free, JSON-native equivalent.
  • Breaking: removed POST /api/v1/external/companies/{companyId}/configuration/export — wrote current config to an existing Google Spreadsheet. Use POST /configuration/export-json (returns current config as a structured JSON body) instead; its output is the exact shape import-json accepts, so export-json → edit → import-json is a lossless round-trip.
  • Unaffected: the in-app onboarding spreadsheet import/export UI. The company.configuration:import / :export permissions are unchanged (still used by the JSON endpoints and the seed surface).

Restructured the seed surface; removed the spreadsheet Generate endpoint

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

Documentation: object-primitive corrections (no API change)

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

2026-06-25

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

New endpoint POST /api/v1/external/companies/{companyId}/configuration/seed puts a company into a usable FMV1 configuration state directly from the framework’s code-defined starter content — with no Google Spreadsheet and no Google OAuth token. It is the Google-free counterpart to POST /configuration/import: where import reads a spreadsheet, seed materializes the equivalent starter content in-memory and runs it through the same validate → compare → apply pipeline.
  • Request: optional { starterSheet?: string } — which starter module variant to seed (omitted → the product default). A bodyless POST seeds the product default.
  • Response: { success: true, message? } on success; a 400 with an error (no changes applied) if the starter content fails validation.
  • Required permission: company.configuration:import (same as Import; FMV1_CONFIGURATION_MANAGER role).

2026-06-17

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

The by-id update endpoints accepted both PUT and PATCH, both performing a partial merge. Because a partial merge is PATCH semantics — and PUT implies full replacement, which these endpoints do not do — accepting PUT was misleading. PUT has been removed: a PUT to any of these routes now returns 405 Method Not Allowed with an Allow: PATCH response header. PATCH behaviour is unchanged (only provided fields change, an explicit null clears, omitted keys are untouched). Affected endpoints (all under /api/v1/external/companies/{companyId}):
  • PATCH /entities/{entityType}/{entityId}
  • PATCH /notes/{noteId}
  • PATCH /tasks/{taskId}
  • PATCH /files/{fileId}
  • PATCH /folders/{folderId}
  • PATCH /files/{fileId}/placements/{placementId}
Migration: if you were sending PUT to any of these, switch the verb to PATCH — the request body and semantics are identical (they were always a partial merge). PUT was previously documented as a 200 alias of PATCH on the file, folder, task, note, and placement routes; that alias is removed.

2026-06-16

Documentation: accuracy reset across the API reference

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

2026-06-11

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

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

Added: file placements — share one file across entities

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

2026-06-10

Added: file categories

Files can now carry a free-text category label (max 255 characters). There is no configured category list — any non-blank string is a valid category.
  • PATCH /api/v1/external/companies/{companyId}/files/{fileId} accepts an optional category field alongside displayName/folderId: a string sets the label, an explicit null clears it, and an absent field leaves it untouched.
  • The file responses (GET /files/{fileId} metadata and the GET /files list items) already include category (null when unset).

Breaking: Company Files API rebuilt on signed URLs

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

Removed: binary download and multipart upload

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

Added: list files

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

2026-06-07

Removed: QuoteBindError object primitive

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

2026-06-01

Added: Notes endpoints

Notes can now be managed via the external API. Notes are simple text records attached to a top-level Field Model V1 entity (Event, Exposure, Quote, Submission, Person, Organization, Policy). The parent entity type travels as the entityType query parameter on every verb; permission is gated by the parent entity (read for GET, update for write).
  • GET /api/v1/external/companies/{companyId}/notes — List notes for a parent entity (1-based page/pageSize)
  • POST /api/v1/external/companies/{companyId}/notes — Create a note (returns { id }, HTTP 201)
  • GET /api/v1/external/companies/{companyId}/notes/{noteId} — Get a note
  • PATCH /api/v1/external/companies/{companyId}/notes/{noteId} — Update a note’s body
  • DELETE /api/v1/external/companies/{companyId}/notes/{noteId} — Soft-delete a note

New: Tasks API

Added 5 endpoints for managing company tasks at /api/v1/external/companies/{companyId}/tasks:
  • GET /tasks — List tasks (paginated, filter by status, assigneeId, entityType, entityId). Requires company.task:read.
  • POST /tasks — Create a task. Returns { id }. Requires company.task:create.
  • GET /tasks/{taskId} — Get a single task. Requires company.task:read.
  • PATCH /tasks/{taskId} — Partial update (only changed fields; unknown fields rejected). Requires company.task:update.
  • DELETE /tasks/{taskId} — Soft delete. Returns { id, deleted: true }. Requires company.task:delete.
A task has a name, description, status (Not Complete / Complete), ISO 8601 deadline, optional linked entity snapshot ({ type, id, name }), and assignees (company user IDs — non-members are rejected with 400). See the Tasks overview.

Breaking: Full-term policy transaction reshape

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

2026-05-15

New: Company Files API (folders + files)

Added 10 endpoints for managing company-level folders and files: Folders: POST, GET, GET /{folderId}, PATCH /{folderId}, DELETE /{folderId} under /api/v1/external/companies/{companyId}/folders Files: POST (multipart upload), GET /{fileId}, GET /{fileId}/content (binary download), PATCH /{fileId}, DELETE /{fileId} under /api/v1/external/companies/{companyId}/files Key capabilities:
  • Create nested folder hierarchies with parentFolderId
  • Upload files via multipart/form-data, optionally placing them in a folder
  • Stream binary file content with correct Content-Type and Content-Disposition headers
  • Rename and move files/folders (including moving to root by setting parent to null)
  • Recursive soft-delete of folders (deletes all contents)
All endpoints use company.file:{action} permissions. See the Company Files overview 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}/events and /events/{eventId} responses include eventPolicy inside fieldModelV1Data alongside the pre-existing top-level policyId. No fields were removed.
  • POST and PUT event endpoints continue to accept policyId as a top-level request param. The server maps it to fieldModelV1Data.eventPolicy internally; clients that already submit policyId need no changes.
  • policyId is no longer stored on a dedicated events.policy_id column on the FMV1 read/write paths — it now lives in fieldModelV1Data.eventPolicy, consistent with how eventInsureds was migrated previously. The external API contract is unchanged for existing integrators.
This is a non-breaking change. Integrators can ignore eventPolicy and continue using policyId, or migrate to reading the value from fieldModelV1Data.eventPolicy to align with the rest of the field model.

Address.zipCode must be a JSON string (breaking)

Address object-primitive writes that send zipCode as a JSON number are now rejected with 400 and problemCode: "InvalidAddressZipCode". Previously, numeric ZIPs were accepted by the API but silently lost their leading zeros — 02140 parses as 2140, corrupting the stored address. Affected endpoints: every FMV1 create/update endpoint that can carry an Address value (top-level field or nested inside a custom object), including all exposure, event, quote, policy-transaction, and custom-object writes. What to send: quote ZIP codes in your payload — "zipCode": "02140", never "zipCode": 02140. The Address sub-field reference 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 zipCode JSON type from a sample value rather than the schema (which has always been type: string).
  • LLM-generated requests that “helpfully” unquote numeric-looking strings.

2026-04-29

Documentation: Object Primitives

  • New Object Primitives reference page documenting the FMV1 built-in object shapes — sub-field tables, JSON examples, the completeness rule, and List cardinality.
  • OpenAPI spec now exports reusable object-primitive schemas under components.schemas for client-codegen consumers.
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}/exposures and /exposures/{id}
  • POST/PATCH /api/v1/external/companies/{companyId}/events and /events/{id}
  • POST/PATCH /api/v1/external/companies/{companyId}/quotes and /quotes/{id}
  • All segmented policy transaction endpoints (new-business, endorse, renew)
  • POST/PATCH /api/v1/external/companies/{companyId}/custom-objects/{objectType} and /{objectId}
What changed:
  • A null, undefined, or empty-string ("") value on a required sub-field of a present object-primitive value now returns 400 with Field '<parentField>' is missing required sub-field '<refId>'. Previously, partial object primitives were silently accepted and surfaced as confusing rating errors downstream.
  • Numeric 0 (e.g. Currency.value: 0) and Boolean false are still valid — the rule only treats null / undefined / "" as missing.
  • Whitespace-only strings (e.g. " ") are NOT treated as missing by this validator. A consumer sending county: " " will pass this check; downstream rating may still reject it. Trim/normalize sub-field strings client-side before submitting.
  • Custom-object sub-fields keep their existing requiredCondition rules unchanged.
  • Omitting the parent object-primitive field entirely is unchanged — the strict rule only fires when the parent value is provided.