{entityType}:
The
{entityType} path segment is the lowercase kebab-case slug (exposure,
not Exposure); the PascalCase form is rejected. The set of available fields for
each type is configured per company — discover it via the /configuration
endpoint below.
A Quote PATCH that changes quoteStatus to
complete runs the bind’s checks: the resolved billing against the actual bind
price, invoice conservation, and the quote’s
intended layer. A
refusal refuses the entire update: data, status and relationships remain
unchanged and no completion form packet is created. Stated receivables must match
pricing, and a quote with no intended layer, or one its bind would miss, cannot
be completed (422 quote-pricing-stack-intended-layer-required or
422 quote-pricing-stack-intended-layer-mismatch).
If the source policy or lifecycle date changes during financial preflight, the
request returns 409 finv2-quote-complete-preflight-changed; reload and retry.
Any quote update, the one that completes it included, may also send
entityUpdateOptions.invoices: "generate", which brings the quote’s invoices
up to date with its billing before those checks run
(Keeping invoices up to date). To take a
quote from creation to a bound policy, see
Rate, Quote, Bind.
Two entities add action endpoints alongside the CRUD surface, because their
work is more than setting a field. An event closes and re-opens through the
dedicated Event Lifecycle endpoints.
eventStatus is system managed, so a value sent for it on a generic create or
PATCH is ignored — see Flow-written status
fields.
An exposure is screened against OFAC sanctions lists through the
OFAC Screening endpoint. Creating or
updating an exposure here stores its screening request values like any other
field data and runs no screening — no provider call, no result.
Every entity’s linked email is read, and what links to it changed, under
/entities/{entityType}/{entityId}/email-…; see
Record Emails.
Recommended forms on quote create
Creating a quote withPOST /api/v1/companies/{companyId}/entities/quote also
attaches the quote’s recommended quote forms: the company’s auto-added
forms rules run once against the new quote, and
every matching Quote form is attached in rank order, as building the quote in
the app does. Its recommended policy forms are added when the quote is
bound with bindForms: true (Bind Quote,
or Bind Quote by Reference or by Value
with a quoteId), from the quote as it stands then, as the app adds them on
the bind flow’s Policy Forms step. They’re in the bind packet, filled with the
bound policy’s values, including the policy number, and the bind’s response
lists them in addedForms. A bind with bindForms: false adds none.
- Each kind’s rules run once. An idempotent replay adds nothing, and a form
removed later stays removed. A later
PATCHadds no quote forms, even when it changes an answer a rule depends on; aPATCHbefore the bind does count for the policy forms, since the bind runs their rules. - A bind with
bindForms: trueadds policy forms only if they were never added, so a quote whose policy forms were added in the app gets none, and a quote built in the app and bound through the API gets the ones it never had. A bind by value adds none. - Forms are added only when the caller also holds
form.createandform.edit. A create without them adds no quote forms, and a bind withbindForms: truewithout them is refused with403 BIND_FORMS_FORBIDDEN. Quote forms are added only for a quote createdinProgressorcomplete. - Send
entityCreateOptions.autoAddForms: falseto create the quote without them; its bind then adds none either, even withbindForms: true. The value is a JSON boolean: anything else, the string"false"included, returns400 INVALID_AUTO_ADD_FORMSand creates nothing. - If the quote forms cannot be added, the quote is still created, and the
response is the same. If a bind with
bindForms: truecannot add the policy forms, it returns500 POLICY_FORMS_NOT_ADDEDand nothing is bound: retry it, or bind withbindForms: false. Nothing is half-attached: either every matching form of a kind is attached or none is. A quote changed while its own bind is adding its policy forms is refused with409; bind it again.
Page layouts for external renderers
Read one configured page withGET /api/v1/companies/{companyId}/pages/{pageKey}/layout, using a fixed page key
such as eventViewDetails, quoteViewDetails, or policyViewDetails.
The call requires the page entity’s view permission (event.view, quote.view,
or policy.view in these examples), without configuration.view.
The response includes that page’s cards, placements, rendering arguments, and
unevaluated display/required conditions, including reachable nested cards.
It excludes unrelated pages and record values. Read field schemas through the
entity configuration endpoint (or /policies/configuration) and fetch records
through their own authorized endpoints.
Layout access does not grant permission to create, edit, bind, or run any other
action. Display conditions are presentation rules, not access controls.
The configuration-authoring describe/cards and describe/card endpoints
continue to require configuration.view.
The six entity types
There are exactly six CRUD entity types. Each links to its configuration schema:Policy is not a CRUD entity. Policy has a dedicated read-only
/policies/configuration
schema, but policy is not valid on the parametric entity-configuration route
and there is no generic policy create/update/delete endpoint. Policy writes go
through the Policy Transaction endpoints.
It is intentionally absent from the CRUD surface and from the /entity-types
discovery response.Response envelope
Get and list responses use the generic entity envelope — identical for every entity type. All entity-specific field values live insidefieldModelV1Data
(keyed by field referenceId); the envelope adds only system metadata:
There is no top-level
companyId and no cross-entity enrichment — linked entities
are not embedded; query them separately. List responses wrap the envelopes as
{ items: [...], hasMore, totalCount }.
createdAt/updatedAt are epoch-second integers, not ISO strings.
Request bodies
A create sends the record’s fields inentityData, and an update
sends the fields to change there: an object whose keys are field
referenceIds from the company’s field configuration. The request’s options,
if it sends any, go in a second object beside it: entityCreateOptions on a
create, entityUpdateOptions on an update. The shape is the same for every
entity type:
POST /api/v1/companies/{companyId}/entities/exposure
referenceIds (and which are required) via the
/configuration endpoint. Date fields are objects, not plain strings:
{ "date": "YYYY-MM-DD", "timezone": "America/New_York" }.
Create returns { id }; delete returns { id, deleted: true }.
Create options
entityCreateOptions is optional, and so is each option in it:
POST /api/v1/companies/{companyId}/entities/quote
Idempotency-Key, Idempotency-Owner
or Auto-Add-Forms header returns 400 ENTITY_OPTION_MOVED, and its message
names the option to send instead, or, for Auto-Add-Forms on an entity type
other than a quote, says to remove the header.
Update options
An update has two options, for a quote:entityUpdateOptions.invoices: "generate" brings the quote’s invoices up to date with its billing once the
update saves, on any quote update, Complete included, and
entityUpdateOptions.invoicesForce consents to what regenerating would
replace
(Keeping invoices up to date).
PATCH /api/v1/companies/{companyId}/entities/quote/{quoteId}
expectedEntityHash, expectedConfigurationHash and
expectedInvoicingEnabled, are query parameters, not options.
Body refusals
A body is checked before anything is written. These return400:
Batch create
Create Entity Batch creates a linked set of new records in one request: exposures, submissions, and new-business or renewal quotes. It saves every record or none of them, so a refused record leaves nothing behind. Each entry names itself withref. A later entry links to it by writing
"ref:<name>" where a record id goes: in a relationship field, such as a
quote’s referencingSubmission, or as the id of an embedded exposure.
Entries save in the order they are listed, so a link names an earlier entry.
The response gives each entry’s new id.
POST /api/v1/companies/{companyId}/entities/batch
- Each entry is a create. An entry is checked and saved as Create Entity
saves one record. A refused entry answers with the error that create would
return, plus
entity: { index, ref, entityType }naming the entry. - One key for the batch. A save needs
batchCreateOptions.idempotencyKey, a UUID. A retry with the same key and body returns201with the same ids andcreated: false. An entry takes noidempotencyKey,idempotencyOwnerorpricingSetupof its own; set a quote’s pricing setup once the batch is saved. - Dry run.
?dryRun=trueruns every check a save runs and saves nothing:200when the save would succeed, or the error it would return. A dry run takes no key. - After the save. A quote’s recommended forms and automatic OFAC screening start once the whole batch is saved. A refused batch or a dry run starts none.
- Limits and permissions. A batch holds up to 2,100 entries and needs the create permission of every entity type in it.
400:
PATCH merge semantics (never PUT)
Updates arePATCH only. The handler does a partial merge of
entityData onto the existing fieldModelV1Data:
- A field provided with a value → updated.
- A field set to
null→ cleared. - A field omitted → left unchanged.
closedDate and eventOpenCloseHistory follow the event’s
lifecycle instead: null can be refused, and a new closedDate moves the
event’s history — see Editing the Closed
Date.
Configuration discovery
GET /entities/{entityType}/configuration returns a JSON Schema (draft
2020-12) describing the fields available for creating or updating that type;
option-set fields carry an enum/oneOf of valid values. Every field is
included — the schema is deliberately complete so you can read the whole shape —
but each field advertises its write tier so you know which ones are yours to
send. Embedded objects (custom objects) appear here as nested Object/Object List properties under their join field.
The required array is your create contract: exactly the fields you must
supply, with everything the platform produces for you already subtracted (join
fields, calculated fields, server-seeded defaults). Supply all of them and a
create cannot be rejected for missing input; omit any and you get one
InvalidEntityShape 400 listing every field you missed, before anything is
written. It is not a PATCH contract — an update merges only the fields you
send in entityData.
The parametric configuration endpoint accepts the six CRUD entity slugs; it does
not accept policy. Use GET /policies/configuration for the read-only policy
field schema. Policy has no generic create endpoint, so that schema’s required
list reflects the fields your configuration marks required in the UI rather than
a generic entity-create contract.
Field write tiers
Each field property carries the marks below.readOnly is the standard JSON
Schema flag a generic tool keys off; the x-* companions say why precisely.
A field may carry more than one mark (a value that is both calculated and
system-owned is marked as both). The rule of thumb: if a field is readOnly,
do not send it; otherwise it is yours to set — including a computed-default
field, which is calculated but caller-wins. The one exception is a field also
marked x-create-input: true, which a create accepts (an event’s openedOn):
the platform turns the value into the data the field is calculated from, and on
an update it is recalculated like any other calculated field.
Sending a readOnly field anyway is not an error, and it does not set the
field. The value is dropped without a warning: the write returns 201 or
200 as usual, and the response does not list the dropped key. The
server-managed value wins (apart from an x-create-input field on create,
above). To catch this, compare what you plan to send against the
configuration’s readOnly marks before you write, or read the record back. The
write must still pass calculation and other validation checks. A value for a
field the configuration does not declare at all still fails the request.
In short, for a create:
required applies only to a create. On a PATCH, any field that is not
readOnly may be sent.
Every calculated field also carries
x-repeatable: whether its expression is
repeatable — same stored data, same answer (true) — or may answer differently
on each evaluation (false — a sequence mint, a clock read). A LIVE field is
always repeatable. System ownership wins over every other mark.
A calculated field also carries x-derived-from when its value is calculated
from other fields of the same entity: the referenceIds of those fields, which
are the ones to set when you want a different value. Only fields the same
schema publishes are listed; a value the calculation reads through a linked
record or a function is not.
A field write group (x-write-permission) is a different mark: a value for a
locked field is never dropped, and a write that names it without the group is
refused with 403 field-write-permission-required.
The MCP server’s describe_api tool returns the same facts in its slimmed
companyFields schema, as readOnly, calculated, variant, repeatable,
derivedFrom, systemOwned, createInput, and required.
Setting the primary insured
In most configurations a submission’s and a quote’sprimaryInsured (and
primaryInsuredName) are calculated: readOnly, x-variant: LIVE, and
x-derived-from: ["exposures"]. The value is the exposures item whose
policyRelationshipType is primary. So a create that sends
"primaryInsured": "<exposureId>" returns 201 and leaves primaryInsured
empty. Send the insured as an embedded exposure instead:
POST /api/v1/companies/{companyId}/entities/submission
id and send the
exposure’s fields, such as exposureName and exposureType, with
"policyRelationshipType": "primary". On a PATCH, exposures restates the
whole list, so send every exposure the record keeps.
Calculated-field validation
When a configured calculation runs during a save, a malformed expression, an evaluation error, or an invalid result rejects the entire write. Invalid results include values that violate the field’s schema (including nested members), nonfinite numbers, and values that cannot be stored as JSON. Entity creates and updates, embedded exposures, and Policy transactions follow this rule. Changes made within the failed write are rolled back. The API returns HTTP400 with error code InvalidFieldModelV1Data; its message
identifies the entity, affected field path, and cause. Use that information to
correct the configured formula or the data it depends on before retrying. A
calculation can fail even when you did not supply the calculated field yourself.
A valid null or missing result still stores null, subject to other validation
rules, including required-field rules. This does not change validation of values
you supply directly or when calculations run:
LIVEfields recalculate on writes where their expressions can run.STAMPEDandYIELDING_STAMPEDfields calculate when their value is null or missing; an existing value is validated and retained without rerunning its formula.- An absent parent object or list item has no child calculation to run.
- Calculations with a declared preview keep that behavior when the context requires it. For example, a calculation requiring an entity ID before one has been allocated keeps an eligible saved value or returns null; allocating the ID later does not rerun it. Browser previews and read-only computation retain their existing behavior.
Flow-written status fields
A status field can be published and still not be yours to move. Two mechanisms do that, and they differ in what happens when you try.eventStatus is a system managed field. A value you send for it is silently
ignored on every generic channel — create and PATCH alike — and the status
follows the event’s open/close-history log instead. You get a 200, not an
error.
The reason is that the status does not travel alone. Closing an event also
stamps its Closed Date and appends to the event’s open/close-history log, and the
lifecycle dates a claim reports — opened on, previously closed on, re-opened on —
are read from that log. A status moved on its own would leave the log silently
disagreeing with the claim. So the transition has its own endpoints — Close
Event and Re-open
Event — which do all of it in one
transaction; see the Event
Lifecycle section. To load a claim
that was already closed, create it with its openedOn and closedDate, or with
its full eventOpenCloseHistory, rather than its status: the platform builds or
checks the log and derives the status from its last entry. On a closed event
closedDate stays editable, and an edit moves the log’s last close with it. A
PATCH that sends the whole log sets the status from its last entry, so it can
close or re-open the event — see Editing the Closed
Date.
quoteStatus and submissionStatus are guarded instead: a generic update
that moves them is refused with 409 (GuardedStatusFieldWrite), and the
message names the flow that owns the transition. Sending the value the field
already holds is always accepted, so an update that merely echoes a status is
unaffected.
Move those statuses through the dedicated lifecycle endpoints instead:
POST /api/v1/companies/{companyId}/submissions/{submissionId}/closecloses the submission and cancels its active quotes atomically.POST /api/v1/companies/{companyId}/submissions/{submissionId}/reopenrecomputes the submission status from its live quotes.POST /api/v1/companies/{companyId}/quotes/{quoteId}/cancelcancels an unbound active quote.
409, while the generic status-field guard remains in place.
Entity-type discovery
GET /entity-types is the top-level discovery endpoint. It returns the fixed set
of six CRUD entity-type descriptors — each with display names, links to its
/entities/{entityType} collection and /entities/{entityType}/configuration
schema, and the per-action permission keys a client needs. It takes no inputs
beyond the path companyId and requires no permission — any API key of the
Company may call it. Use it to bootstrap a client that doesn’t already know
the entity model.
Embedded exposures
Some entity types embed exposures inline rather than only linking them by id. A Quote, Policy, or Submission can keep its own copy of an exposure’s data (see Exposure for why), carried in an embedded-exposure field — an array of exposures inside the host’s payload. In the/configuration schema such a field appears as an array whose items are a
oneOf of two modes:
- Reference mode —
required: ["id"]. The item carries theid(uuid) of an existing exposure. Every other exposure field is present but optional; any you supply override that field on the embedded copy. - Create mode — no
id. A new exposure is created inline. Itsrequiredis the caller-required Exposure create contract: the same fields a standalone exposure create requires after calculated values, server-seeded defaults, and joins are removed. Join fields (e.g.referencingPolicies,contacts) are never required.
InvalidEntityShape 400 whose userMessages list every missing field for that
item. Each message includes the item’s path (for example exposures[1]), and the
host transaction rolls back without persisting the host or any inline-created
exposure.
Both modes carry the same field write tiers as a standalone
exposure — the Exposure’s system-owned fields (referencing*) are marked
readOnly + x-system-owned inside each mode, so an embedded item should not
send them either.
Rating and tax output containers (exposureRatingResponse,
crossSegmentRatingOutputs, inscipherTaxPlan) are settable on an embedded
exposure. Hosted rating does not persist its results, so writing them back is how
they are stored — see Rating.
The three payload shapes
Which mode applies is decided purely by payload shape — whether
id is
present — never by who is calling. An already-embedded item keyed by an id the
host already holds re-uses the stored copy as its base (it is not re-copied
from the canonical record); any fields you send still overlay it.
Membership is a restatement
Providing an embedded-exposure field restates the complete membership of that field (a JSON-Merge-Patch convention):- Items present are kept (referenced) or created.
- A stored item omitted from the array is removed from the host.
- Omitting the field entirely is a no-op — the stored membership is untouched.
As-if-inserted validation
After the copy/override merge, every embedded item is validated as if it were being inserted as a standalone exposure against the current Exposure configuration — field shapes, calculated-value resolution, tenant invariants, type casting, and entity invariants, the same checks a standalone exposure create runs. This applies in reference mode too: embedding pre-existing canonical data that violates today’s configuration fails the write. A failure is a structured400 whose message names the offending item by
path (e.g. additionalExposures[1]):
Permissions
Permissions follow the format<resource>.<verb> (verb ∈
view | create | edit | delete | export) — each entity type is its own
catalog resource, so the key is a straight function of the type. Treat these
as the source of truth (and confirm at runtime via the per-type permissions
block in the /entity-types response):
The per-type
/configuration endpoint uses the same read permission as the
entity itself (e.g. event.view for Event, exposure.view for Exposure) —
see the Get Entity Configuration
endpoint.
Export presets
An export preset is a saved, named column selection for one entity type’s export screen: which columns to export, in which order. The Create Export Preset For Member endpoint creates one on behalf of a company member you name withownerUserId, and requires export-preset.create-for-member. The preset is
private to that member, who finds it in their own export screen for that entity
type. Every field key is validated on write against the columns the company’s
export surface offers, so an unknown key is refused rather than saved.
entityType: "bordereau" creates a preset for the Export Bordereau screen; its
keys are fixed:<key> for a built-in bordereau column and field:<key> for a
column configured on the bordereau export surface.
A preset can also export one row per item of an Object List field, as the
export screen’s “One row per” choice does: send rowSource with the list’s
field, and fieldKeys may then include that list’s item columns and the item
identity columns __itemId and __itemIndex. The rowSource must be an
Object List that at least one export-surface column declares; anything else is
400 InvalidExportRowSource.
Three more endpoints act on a member’s existing presets, each with its own
permission:
Replace and delete find the preset by its
id in the company, whoever owns it.
Listing, filtering & sorting
The list endpoint supports:
That table is the whole list. Any other query parameter is rejected with
400 InvalidRequest, and the message names the ones the endpoint accepts.
This matters most for paging, which has exactly one spelling here —
pageNumber — so ?page=, ?offset=, ?skip= and ?cursor= are errors. They
used to be ignored, which meant a book larger than one page could not be walked
at all: the endpoint kept returning the first page with hasMore: true, and a
page=N loop re-read it forever without ever failing.
Walking a whole book
Start atpageNumber=0 and increment until hasMore is false. Pass an
explicit sortBy/sortDirection so the ordering is fixed for the duration of
the walk — the default sort is updatedAt, which a concurrent write can move
under you.
Updating a bound quote
A quote whosequoteStatus is bound no longer accepts updates. PATCH /entities/quote/{quoteId} on one is refused with 409 and code
QuoteBoundImmutable, whatever the body holds, and the error carries
referencingPolicy so you have the policy id without a second call.
The reason is that there is nothing an edit could accomplish: a policy’s values
live in its own segments, written once by the bind, and nothing re-reads the
quote afterwards. An accepted edit would only move the quote away from the
policy it is supposed to describe. To change what is in force, endorse the
policy (POST /policies/{policyId}/transaction/endorse). To model a variation,
create a new quote.