curl --request POST \
--url https://go.aiinsurance.io/api/v1/companies/{companyId}/configuration/data-validation-runs \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"fields": [],
"pages": [],
"cards": [],
"cardPageRelationships": [],
"optionSets": [],
"customObjects": [],
"fieldLocations": [],
"ratingWorkflows": [],
"entityInvariants": []
}
'import requests
url = "https://go.aiinsurance.io/api/v1/companies/{companyId}/configuration/data-validation-runs"
payload = {
"fields": [],
"pages": [],
"cards": [],
"cardPageRelationships": [],
"optionSets": [],
"customObjects": [],
"fieldLocations": [],
"ratingWorkflows": [],
"entityInvariants": []
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
fields: [],
pages: [],
cards: [],
cardPageRelationships: [],
optionSets: [],
customObjects: [],
fieldLocations: [],
ratingWorkflows: [],
entityInvariants: []
})
};
fetch('https://go.aiinsurance.io/api/v1/companies/{companyId}/configuration/data-validation-runs', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://go.aiinsurance.io/api/v1/companies/{companyId}/configuration/data-validation-runs",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'fields' => [
],
'pages' => [
],
'cards' => [
],
'cardPageRelationships' => [
],
'optionSets' => [
],
'customObjects' => [
],
'fieldLocations' => [
],
'ratingWorkflows' => [
],
'entityInvariants' => [
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://go.aiinsurance.io/api/v1/companies/{companyId}/configuration/data-validation-runs"
payload := strings.NewReader("{\n \"fields\": [],\n \"pages\": [],\n \"cards\": [],\n \"cardPageRelationships\": [],\n \"optionSets\": [],\n \"customObjects\": [],\n \"fieldLocations\": [],\n \"ratingWorkflows\": [],\n \"entityInvariants\": []\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://go.aiinsurance.io/api/v1/companies/{companyId}/configuration/data-validation-runs")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"fields\": [],\n \"pages\": [],\n \"cards\": [],\n \"cardPageRelationships\": [],\n \"optionSets\": [],\n \"customObjects\": [],\n \"fieldLocations\": [],\n \"ratingWorkflows\": [],\n \"entityInvariants\": []\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://go.aiinsurance.io/api/v1/companies/{companyId}/configuration/data-validation-runs")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"fields\": [],\n \"pages\": [],\n \"cards\": [],\n \"cardPageRelationships\": [],\n \"optionSets\": [],\n \"customObjects\": [],\n \"fieldLocations\": [],\n \"ratingWorkflows\": [],\n \"entityInvariants\": []\n}"
response = http.request(request)
puts response.read_body{
"runId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"outcome": "enqueued"
}Start Data Validation Run
Starts a data validation run: a background scan that reads every stored
record of every entity type for this company and classifies each one against
a configuration. Returns immediately with a runId — poll
GET /configuration/data-validation-runs/{runId} for progress and results.
This is the data-aware companion to validate. Validate checks whether a configuration is well-formed and reads no records at all; a run checks whether the records you have already stored still fit a configuration.
The request body is OPTIONAL — that is how you choose what to scan
- Send no body at all → the run scans the company’s live
configuration. This answers “do my current records still conform to the
config I am running right now?” Nothing is read from the request; the live
configuration is snapshotted onto the run. An explicit JSON
nullbody, and an empty JSON object, are accepted the same way and also mean a live scan. - Send a COMPLETE configuration body (the same shape import and validate accept) → the run scans that candidate configuration. Nothing is persisted as the company’s configuration — the body is only snapshotted onto the run — so this is a safe way to ask “which of my records would break if I imported this?” before importing it.
Either way the configuration is snapshotted onto the run when it starts, so a run’s verdict cannot drift: a configuration import that lands mid-scan does not change what the scan is measuring against.
A delta/patch body is rejected with a 400. Typed changes go to the patch
endpoint (POST .../configuration/patch); this endpoint, like import and
validate, takes a complete body only.
Send the body as application/json, or send nothing
A body that arrives and cannot be read as JSON is a 400
(UNREADABLE_REQUEST_BODY) — it is never treated as an omitted body. This
covers a body sent with a Content-Type other than application/json, a body
sent with no Content-Type at all, and a body that is not valid JSON (a
truncated or corrupted payload). Failing loudly is the point: silently falling
back to a live scan would return a clean result for a candidate configuration
nobody ever measured.
force is a query parameter only. A force key inside the request body is
part of the configuration document, not a control flag, and is ignored — use
?force=true.
Duplicate starts collapse onto the run already in flight
If a run is already in flight (queued or running) for this company and it is
scanning the exact same configuration, this endpoint does not start a
second scan. It returns 200 with that run’s id and outcome: "duplicate".
duplicate is a normal, successful answer and not an error — a client
looping starts, or retrying after a timeout, should treat it as “the scan you
asked for is already happening, poll this id”. Configurations are matched by
content, so a body that differs anywhere is a different scan and starts a new
run. Pass ?force=true to start a new run even when a matching one is in
flight.
At most 3 runs in flight per company
A company may have 3 runs in flight (queued or running) at once. A 4th
distinct configuration is refused with a 429 whose body carries
inFlightRunIds — the ids of the runs already in flight — so you can poll those
instead of retrying blind. Wait for one to reach a terminal status, then start
again.
?force=true does not lift this limit; it only overrides the duplicate
check described above. The limit exists because scans share one background work
queue with everything else the platform runs (parsing, extraction, invoicing),
so an unbounded number of scans for one company would starve unrelated work.
Required permission: configuration.validate
This endpoint requires an API key that holds the configuration permissions — the Admin role does. See Authentication for how to create API keys with specific roles.
curl --request POST \
--url https://go.aiinsurance.io/api/v1/companies/{companyId}/configuration/data-validation-runs \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"fields": [],
"pages": [],
"cards": [],
"cardPageRelationships": [],
"optionSets": [],
"customObjects": [],
"fieldLocations": [],
"ratingWorkflows": [],
"entityInvariants": []
}
'import requests
url = "https://go.aiinsurance.io/api/v1/companies/{companyId}/configuration/data-validation-runs"
payload = {
"fields": [],
"pages": [],
"cards": [],
"cardPageRelationships": [],
"optionSets": [],
"customObjects": [],
"fieldLocations": [],
"ratingWorkflows": [],
"entityInvariants": []
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
fields: [],
pages: [],
cards: [],
cardPageRelationships: [],
optionSets: [],
customObjects: [],
fieldLocations: [],
ratingWorkflows: [],
entityInvariants: []
})
};
fetch('https://go.aiinsurance.io/api/v1/companies/{companyId}/configuration/data-validation-runs', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://go.aiinsurance.io/api/v1/companies/{companyId}/configuration/data-validation-runs",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'fields' => [
],
'pages' => [
],
'cards' => [
],
'cardPageRelationships' => [
],
'optionSets' => [
],
'customObjects' => [
],
'fieldLocations' => [
],
'ratingWorkflows' => [
],
'entityInvariants' => [
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://go.aiinsurance.io/api/v1/companies/{companyId}/configuration/data-validation-runs"
payload := strings.NewReader("{\n \"fields\": [],\n \"pages\": [],\n \"cards\": [],\n \"cardPageRelationships\": [],\n \"optionSets\": [],\n \"customObjects\": [],\n \"fieldLocations\": [],\n \"ratingWorkflows\": [],\n \"entityInvariants\": []\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://go.aiinsurance.io/api/v1/companies/{companyId}/configuration/data-validation-runs")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"fields\": [],\n \"pages\": [],\n \"cards\": [],\n \"cardPageRelationships\": [],\n \"optionSets\": [],\n \"customObjects\": [],\n \"fieldLocations\": [],\n \"ratingWorkflows\": [],\n \"entityInvariants\": []\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://go.aiinsurance.io/api/v1/companies/{companyId}/configuration/data-validation-runs")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"fields\": [],\n \"pages\": [],\n \"cards\": [],\n \"cardPageRelationships\": [],\n \"optionSets\": [],\n \"customObjects\": [],\n \"fieldLocations\": [],\n \"ratingWorkflows\": [],\n \"entityInvariants\": []\n}"
response = http.request(request)
puts response.read_body{
"runId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"outcome": "enqueued"
}Authorizations
User-principal OAuth 2.0 Bearer authentication. Send a user-scoped Auth0 access token (audience = the app API audience) as Authorization: Bearer <jwt>. The request resolves to the user's identity and is authorized by their Role on the {companyId} in the path — the same role-based permissions the web app enforces. This is the path the MCP connector uses to act on a user's behalf; endpoints that accept it list both BearerAuth and ApiKeyAuth.
Path Parameters
Company identifier
Query Parameters
When true, start a new run even if a run is already in flight for this
company scanning the same configuration — i.e. skip the duplicate check and
always mint a fresh run. Any value other than true (including omission)
is treated as false.
This does not raise the limit of 3 runs in flight per company: a
?force=true request past that limit still gets a 429.
Unlike force on import, this needs no additional permission and
destroys nothing — the only cost of forcing is a redundant scan.
Body
OPTIONAL. Omit the body entirely to scan the company's live configuration — that is the documented way to ask for a live scan, and no query flag is needed for it.
When a body IS sent it must be a COMPLETE configuration document matching the
schema below, and the run scans that candidate configuration instead;
nothing is persisted. A delta/patch body is rejected with a 400, and so is a
body that cannot be read as JSON.
- Option 1
- Option 2
Accepts either the existing split definitions or unified customObjects and optionSets. Do not mix formats. Both inputs produce the same stored configuration; exports return unified definitions.
Field definitions, keyed by entity + reference id.
One top-level field definition, keyed by entity + reference id. Declare its
type with structured typeInfo. Legacy type and string-encoded typeArgs
remain accepted; when both representations are supplied they must agree
semantically. A conflict is rejected with the field identity.
Exports use typeInfo without the legacy duplicate properties.
- Option 1
- Option 2
Show child attributes
Show child attributes
Page definitions.
Show child attributes
Show child attributes
Card definitions.
Show child attributes
Show child attributes
Placements of cards onto pages.
Show child attributes
Show child attributes
Option-set type declarations.
Show child attributes
Show child attributes
Custom-object type declarations.
Show child attributes
Show child attributes
Option sets and their options.
Show child attributes
Show child attributes
Custom-object sub-field definitions, joined to object types.
Show child attributes
Show child attributes
Field placements (the layout) onto cards.
Show child attributes
Show child attributes
Rating workflow definitions.
Show child attributes
Show child attributes
Per-entity invariant conditions enforced on every write.
Show child attributes
Show child attributes
Forms-logic rules (quote-flow auto-add rules). Optional — existing payloads predate the "Forms" tab; absent ⇒ no rules.
Show child attributes
Show child attributes
Smart tags — the named values resolved into generated documents. Optional: existing payloads predate the slice, and absent ⇒ no smart tags, which is also how a company that has not yet moved to this format is recognised.
Omitted from an export when the company has none, rather than emitted as an empty array.
Show child attributes
Show child attributes
Declared export columns — the tenant-facing column set of each export surface (the seven entity exports plus the bordereau). Optional: existing payloads predate the slice, and absent ⇒ no declared columns, which is also how a surface that still offers every configured field is recognised.
Activation is per surface: a surface with at least one row here resolves its whole tenant column set through those rows, in the order they appear; a surface with none behaves exactly as it did before this section existed.
Omitted from an export when the company has declared none, rather than emitted as an empty array.
One declared export column — a tenant-facing column on one export surface.
Mirrors the ExportSurfaceRowDefinition shape.
A surface is one column set: each of the seven entity exports, plus the
bordereau. bordereau reads Policy fields like the policy surface does, but
it is a separate report with its own fixed columns, so it is its own surface.
A column's identity is the (surface, key) PAIR: there is one row per
surface, so the same key on two surfaces is two independent columns that may
disagree about everything else. Labels are therefore unique within a surface,
not company-wide.
Columns are explicitly declared. A surface with no declarations exports only its platform columns. An optional rowSource makes a declaration available when exporting one row per item of that saved Object/List field.
COLUMN ORDER IS CANONICAL, not authored: the stored configuration sorts this
section by (surface, key), so the order rows appear in a request body does
not survive the round trip. Read a column's position off its key, never off its
position here.
A row must carry spreadsheetType, or both valueType and cardinality.
- Option 1
- Option 2
Show child attributes
Show child attributes
Declared list columns — the complete tenant-facing column set of each entity list page. Optional: existing payloads predate the slice, and absent ⇒ no declared tenant columns, so each list renders only its platform/system columns.
Rows declare membership, not order or default visibility. The platform's
per-page defaults and each user's saved view own those choices; the stored
configuration sorts rows canonically by (surface, key).
Omitted from an export when the company has declared none, rather than emitted as an empty array.
Show child attributes
Show child attributes
Tenant-defined list filters, one per (surface, key). Together with
page-owned system filters, these rows are the complete user-visible filter
catalog. listColumns contributes no filters.
Optional for wire compatibility with payloads that predate the slice. Absent or empty means that each entity list offers only its page-owned system filters. Omitted from an export when the company has none.
Show child attributes
Show child attributes
Explicitly authored rules determine which values move between Policy and Quote, their destinations, and transformations. Authors maintain each direction and transaction scope independently. Matching field names imply no mapping; an absent optional mapping is valid and causes no conversion write to that destination. A rule may directly assign a source value or transform it, including writing to a differently named destination.
Each rule is identified by its (direction, transactionType,
destinationPath) triple; duplicates are rejected. The stored
configuration sorts the section canonically by that triple. Import
validates explicit rules and requires all framework-pinned rows, including
on first import. Omitting or emptying the section is invalid, even with
force enabled. Preserve the authored slice through whole-configuration imports. Field edits require
reviewing affected rules against business intent.
Show child attributes
Show child attributes
Server-derived memory for retired data addresses. Records have exactly one of three kinds: top-level field, custom-object subfield, or option-set value. A submitted copy is accepted for export/import round trips but is non-authoritative; the server derives the next set from the previous immutable configuration version. Omitted when empty.
- Option 1
- Option 2
- Option 3
Show child attributes
Show child attributes
Spreadsheet types for built-in (platform) export columns, one per
(surface, column). Optional: absent or empty means platform columns
keep their built-in formatting. column must name a built-in column of the
surface (checked by name at import). Omitted from an export when empty.
Show child attributes
Show child attributes
Response
The run to poll. outcome: "enqueued" means a new scan was started;
outcome: "duplicate" means runId is a run already in flight scanning
this same configuration and no second scan was started.
The run to poll after starting a data-validation run. outcome is a policy
verdict delivered as a successful 200, not an error: duplicate means a run
scanning this same configuration was already in flight, so runId is that run
and no second scan was started.
The run to poll via GET /api/v1/companies/{companyId}/configuration/data-validation-runs/{runId}
enqueued when a new scan was started; duplicate when a run already in flight was scanning this same configuration (pass ?force=true to start a new run anyway)
enqueued, duplicate 