Skip to main content
To list policies see the Policy Transactions Api. Policy is not one of the entity types this API serves — policy and policies are rejected here. Listing lives at List Policies (GET /api/v1/companies/{companyId}/policies/list).
Every top-level Field Model V1 entity is managed through one parametric CRUD surface, keyed on {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.
Creating a quote with POST /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 PATCH adds no quote forms, even when it changes an answer a rule depends on; a PATCH before the bind does count for the policy forms, since the bind runs their rules.
  • A bind with bindForms: true adds 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.create and form.edit. A create without them adds no quote forms, and a bind with bindForms: true without them is refused with 403 BIND_FORMS_FORBIDDEN. Quote forms are added only for a quote created inProgress or complete.
  • Send entityCreateOptions.autoAddForms: false to create the quote without them; its bind then adds none either, even with bindForms: true. The value is a JSON boolean: anything else, the string "false" included, returns 400 INVALID_AUTO_ADD_FORMS and 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: true cannot add the policy forms, it returns 500 POLICY_FORMS_NOT_ADDED and nothing is bound: retry it, or bind with bindForms: 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 with 409; bind it again.

Page layouts for external renderers

Read one configured page with GET /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.
Custom objects are embedded-only. A custom object is never a top-level CRUD entity and never appears here or in /entity-types. Custom objects surface only as Object / Object List field definitions inside an entity’s /configuration response (for example, an embedded coverage list on a Quote). You read and write them as nested values on their owning entity — there is no /entities/{customObject} route.

Response envelope

Get and list responses use the generic entity envelope — identical for every entity type. All entity-specific field values live inside fieldModelV1Data (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 in entityData, 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
Discover the valid 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
The create takes no option headers. An 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}
The update’s guards, expectedEntityHash, expectedConfigurationHash and expectedInvoicingEnabled, are query parameters, not options.

Body refusals

A body is checked before anything is written. These return 400:

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 with ref. 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 returns 201 with the same ids and created: false. An entry takes no idempotencyKey, idempotencyOwner or pricingSetup of its own; set a quote’s pricing setup once the batch is saved.
  • Dry run. ?dryRun=true runs every check a save runs and saves nothing: 200 when 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.
A batch’s own refusals return 400:

PATCH merge semantics (never PUT)

Updates are PATCH 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.
An event’s 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.
Never PUT. PUT would imply full replacement, which these endpoints do not do. A PUT to a by-id entity route is rejected with 405 Method Not Allowed and an Allow: PATCH header. Use PATCH.

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’s primaryInsured (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
To create the insured in the same request, leave out 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 HTTP 400 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:
  • LIVE fields recalculate on writes where their expressions can run.
  • STAMPED and YIELDING_STAMPED fields 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}/close closes the submission and cancels its active quotes atomically.
  • POST /api/v1/companies/{companyId}/submissions/{submissionId}/reopen recomputes the submission status from its live quotes.
  • POST /api/v1/companies/{companyId}/quotes/{quoteId}/cancel cancels an unbound active quote.
They enforce the same transition guards as the app: an invalid transition is a 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 the id (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. Its required is 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.
If a create-mode item omits more than one required input, the API returns one 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.
A single-cardinality embedded field behaves the same way, restating the one item.

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 structured 400 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 with ownerUserId, 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 at pageNumber=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 whose quoteStatus 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.