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: company.configuration:export
This endpoint requires an API key created with the FMV1_CONFIGURATION_MANAGER role. See Authentication for how to create API keys with specific roles.
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.
Structured FMV1 configuration body. This is both the body the import
endpoint accepts and the body the export endpoint returns, so
export → (edit) → import is a lossless round-trip. Each property is an
array of config rows; the import runs them through the full validate →
compare → apply pipeline.
The per-element schemas below are the canonical FMV1 configuration model and the single source of truth for this payload.
Field definitions, keyed by entity + reference id.
Page definitions.
Card definitions.
Placements of cards onto pages.
Option-set type declarations.
Custom-object type declarations.
Option sets and their options.
Custom-object sub-field definitions, joined to object types.
Object-primitive sub-field definitions (Address / Date / Currency).
Field placements (the layout) onto cards.
Rating workflow definitions.
Per-entity invariant conditions enforced on every write.
Forms-logic rules (quote-flow auto-add rules). Optional — existing payloads predate the "Forms" tab; absent ⇒ no rules.
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.
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.
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 