Skip to main content
The FMV1 Configuration API provides JSON-native endpoints for exporting and importing company configuration. This is the programmatic interface to the configuration pipeline. A new company starts from a complete configuration document posted to Import Configuration. For Quote–Policy mappings and JEXL transformations, see Conversion Rules. For expression syntax, macro availability, and entity invariant rules, see Author JEXL expressions.

Authentication

These endpoints require an API key that holds the configuration permissions. The Admin role holds them; a role without them returns 403.To create an API key with this role, go to Settings > API Keys and select the Admin role when generating the key.

Workflow

The JSON round-trip workflow is:
  1. Export — Pull current config as a structured JSON body
  2. Edit the JSON
  3. Import — Apply the edited JSON body to the database
export → edit → import is a lossless, machine-to-machine round-trip. For a fresh company, POST the complete initial document directly to Import. Rating workflow stages accept only stageType, raterSpec, callOncePerPath, outputPath, and optional inputs. Validation rejects unknown stage properties with the affected stage’s JSON path, so a misspelled property cannot silently discard input selection or output routing.

API Endpoints

The Configuration Management endpoints are listed in the left navigation:
  • Export Configuration (POST /configuration/export) — Return current config as a structured JSON body. The output is the exact shape import accepts, so export → edit → import is a lossless round-trip.
  • Get Configuration Metadata (POST /configuration/metadata) — Return a stable config version token plus last-modified / last-imported timestamps, without the full config body. Use it for cheap staleness checks before pulling the full export payload.
  • Import Configuration (POST /configuration/import) — Apply a structured JSON configuration body to the database.
  • Start Data Validation Run (POST /configuration/data-validation-runs) — Start a background scan that classifies every stored record against a configuration. Send a complete configuration body to scan a candidate, or send no body at all to scan the live configuration. Returns a runId to poll.
  • Get Data Validation Run (GET /configuration/data-validation-runs/{runId}) — Poll a run’s status and its running counts of records scanned, adhering, holding undeclared keys, and breaking.
  • List Data Validation Run Findings (GET /configuration/data-validation-runs/{runId}/findings) — Page through which records would not survive the scanned configuration and why. Paginated (page / pageSize) because a poll must not re-transfer thousands of findings; filter with entityType to build a per-type view.

Field type declarations

Full configuration imports, validation and comparison accept a field’s structured typeInfo without duplicate type or typeArgs properties. The add-field patch change accepts the same declaration, including when adding a custom-object subfield:
For a full configuration field, use entitySelector instead of entity and omit kind: "add-field". Argless types need only their kind, such as "typeInfo": { "kind": "Text" }. Existing type and JSON-string typeArgs inputs remain accepted. When you send both forms, they must describe the same type and arguments; conflicts identify the field in the error. Configuration exports declare each field type once in typeInfo, without type or typeArgs duplicates. Field values and cardinality rules are unchanged.

Reserved Quote field names

pricingSetup and invoices are reserved names for Quote fields, because each also names a quote option. A quote’s create takes entityCreateOptions.pricingSetup, its pricing setup, and the update that completes a quote takes entityUpdateOptions.invoices, which generates its invoices as it completes. The names stay reserved because those requests refuse them among a quote’s fields: pricingSetup in a create’s entityData, and invoices in an update’s, get 400 ENTITY_OPTION_MOVED, naming the option. Import, validation and patches therefore refuse a Quote field with either name, with an error that names the field. Fields on other entities, and custom-object subfields, may use them.

Card spacing

Any card can set optional rowGap and columnGap to add vertical and horizontal space between its grid rows and columns. Each value is a nonnegative number of pixels. Omit it, or use 0, to keep the existing spacing in that direction. The layout editor exposes Extra row spacing (px) and Extra column spacing (px) in the card properties; previews and the live app use the same settings. For example, this card adds 10 pixels between both rows and columns:
The extra gaps are in addition to the normal spacing: 12 pixels between columns, and 24 pixels from a value or an input to the label below it, the same when viewing and editing. Here, each of those grows by 10 pixels. Other cards keep their existing spacing. Empty rows still collapse; rowGap applies between the rows that remain visible. Column positions and spans stay as authored.

Join picker order

A field location whose inputModality is Join Picker or Pointer Picker can set the order of the picker’s options with inputModalityArgs.sort:
  • field is createdAt (the date the record was created), updatedAt (the date it last changed), or the referenceId of a field on the entity the picker lists. That field sorts as its list column does: numbers by value, dates by day, option-set fields by option label, text case-insensitively. Relationship fields (Join and Pointer) can’t be sorted.
  • direction is asc (oldest, lowest or A to Z first) or desc (newest, highest or Z to A first).
The order applies to every option the picker can offer, after the picker’s other filters (filter, relationshipFilter) and the user’s typed search, so the first options shown are the true top of the list. Options with no value for the field come last in both directions, and records with the same value always appear in the same order. Without sort, the picker keeps its default order: by display name, or best match first while the user’s search covers every field. Import and validation refuse a sort that isn’t an object, a direction other than asc or desc, a field that isn’t createdAt, updatedAt or a sortable field of the listed entity, a sort on a picker of users, and a sort alongside optionsFrom. Each error names the field location and the problem.

Checking configuration against stored data

Validate and compare are configuration-only: neither reads a single stored record, which is why both answer instantly regardless of how much data a company holds. They tell you a configuration is well-formed and how it differs from the current one — not whether the records you already have would survive it. Data validation runs answer that second question. A run reads every stored record of every entity type and classifies each one, so the natural order before a risky import is: validate (is the config well-formed?) → compare (what changes?) → a run (which of my records would break?) → import. Four properties are worth knowing before you build against it:
  • A run snapshots its configuration when it starts, so a verdict cannot drift — an import that lands mid-scan does not change what the scan is measuring against.
  • Starts collapse and are capped. Starting a run whose configuration matches one already in flight returns that run’s id with outcome: "duplicate" instead of starting a second scan (pass ?force=true to override). A company may have 3 runs in flight; a 4th distinct configuration is refused with a 429 carrying inFlightRunIds so you can poll those instead.
  • The counts and the detail are separate reads. The poll is fixed-size — status plus four counts — and findings is a separate, paginated endpoint, so polling a run costs the same whether the book is clean or badly broken. Poll until the run is terminal, then page the findings once.
  • A run’s status and findings stay readable for as long as the run exists. Nothing removes a run or its findings on a timer, so a run started months ago still answers in full. A run’s age is never a failure on either by-id read: neither reports a run as too old to read, and a run id that belongs to no run of this company is a 404.

Permissions


Framework-Required Rows

Some configuration rows are required by the framework: the import rejects a configuration that omits them or changes their type, cardinality, or join shape. Most framework-required rows also have a fixed calculated value the import enforces byte-for-byte, but a few are flagged customer-supplied: the row’s presence and shape are enforced, while its JEXL calculated-value expression is yours to author and maintain. The main example is referencingExposures on Quote, Policy, and Submission. Quote.fullTermBillingInfo is another; see Quote billing calculations.

Quote billing calculations

Every configuration states Quote.fullTermBillingInfo as an always-owned Live calculation, and every company invoices. The calculation is a real one that the browser can evaluate: configuration import and validate refuse a blank calculation or a bare null as the whole calculation, and refuse server-only or non-pure functions, including through other calculated fields it reads. The error identifies the field and function that cannot run in the browser. CLEAR() is a real calculation that bills nothing. Import and validate also refuse a rating workflow stage that writes fullTermBillingInfo, rating workflow billingPreconditions (use billing conditions instead), and a Conversion Rule that writes fullTermBillingInfo other than the pinned billing copy.

Billing conditions

A configuration can declare optional top-level billingConditions. Each named condition has a * default; a row for a specific quote flow replaces its default expression and message. A true override waives the condition for that flow. An empty quote type uses the default.
This fragment belongs in a complete configuration document. Import refuses a duplicate (conditionKey, quoteFlow), an override without its default, or an expression that cannot return a browser-pure boolean. That check includes calculated fields read by the condition. Conditions require a billing calculation on Quote.fullTermBillingInfo. When any selected condition is false, saving and rating skip the billing calculation and return null billing. The Billing step shows the selected condition messages and updates them from unsaved Quote inputs. Satisfying the conditions recalculates billing immediately, without generating rating or refreshing billing. Pricing remains visible while a condition holds billing at null. False conditions do not themselves refuse saving or binding; the ordinary financial integrity checks and the pricing stack’s layer checks still apply. A condition expression that throws remains a calculation error. Use typed patches add-billing-condition-row, update-billing-condition-row, and delete-billing-condition-row to address a row by its conditionKey and quoteFlow. An update changes expression, message, or both; changing its identity requires deleting and adding a row. The complete resulting configuration is validated, so deleting a default while retaining its overrides is refused. Export omits the section when it is absent or empty and sorts declared rows by (conditionKey, quoteFlow).

Broker Portal section

The optional top-level brokerPortal section holds the pages the Broker Portal uses and its decline, assignment and quote settings. It does not turn the portal on; a platform admin does that, and a configuration import cannot. The portal’s logo and colours are not configuration. They are your company’s branding, set in Company Settings, and your emails use them too once you turn on email branding there. Import refuses a branding key in brokerPortal, as it refuses any other unknown key. The typed patches set-broker-portal-decline-copy-to, set-broker-portal-decline-forms and set-broker-portal-assignable-users create the section when the configuration has none. Export omits the section when the configuration has none.

Broker Portal pages

There are five Broker Portal pages. A company has one only when its brokerPortal.pages names it:
Place cards on a named portal page with cardPageRelationships, as on any other page. A card on a portal page the configuration does not name is refused. Only the keys above are allowed in brokerPortal.pages. An app page key such as exposureCreate is refused with its path, for example brokerPortal.pages.1. A key listed twice is refused at brokerPortal.pages. While a company’s Broker Portal is on, the list must include portalRequest: import and validate refuse a configuration without it, and the portal cannot be turned on until the configuration has it. Export lists the app pages in the top-level pages for every company; Broker Portal pages appear only in brokerPortal.pages.

Card sections

A card can set an optional section, a non-empty label. On the Broker Portal’s portalSubmission page, a jump-to section nav lists the card under that label, and clicking it scrolls to the card. A card without section is not listed, and other pages ignore it.
add-card and update-card take section too. Send "section": null on update-card to clear it.

Broker Portal decline copy-to address

brokerPortal.declineCopyTo is an optional email address. When an underwriter declines a Broker Portal request, the decline dialog prefills it in “Send a copy of the application to”. Without it the field starts empty. Use a shared team mailbox, not a personal address: the configuration is versioned and exported.
Import refuses a value that is not an email address. Use the typed patch set-broker-portal-decline-copy-to to set it, or send null to clear it. The patch needs an existing brokerPortal section.

Broker Portal decline forms

brokerPortal.declineForms lists, by form number, the Submission form templates the decline dialog attaches to the copy of the application by default, in attachment order. Each form is generated against the declined submission and attached as a PDF; the underwriter can change the selection before declining. Without it no form is preselected.
Import refuses a number that names no live template of the company, a template whose category is not Submission, and a repeated number. Use the typed patch set-broker-portal-decline-forms to replace the list, or send an empty list to clear it. The patch needs an existing brokerPortal section.

Broker Portal request assignees

brokerPortal.assignableUsers lists the user ids a broker’s request may be assigned to. When a broker submits a request, the Submission’s submissionAssignee is set from it:
  • No users listed: the assignee is left as it is.
  • One user: that user is assigned.
  • More than one: the broker picks one or more of the listed users.
The patch replaces the whole list. It is refused when the configuration has no brokerPortal section yet, so set the branding first, and when an id is not a user of your company. Company Settings in the app edits the same list.

Configuration Categories

The compare endpoint returns change counts for each category. It includes billingConditions only when either compared configuration declares condition rows: