Store rows
/imports/rows. The 201 response contains importRowIds in
request order and importBatch. Each element is stored unchanged as JSON;
objects, arrays, scalars and null are accepted. There are no transformations,
guards, entity writes or configuration checks in this operation. Send up to
1000 rows per request. Repeating ingestion creates new rows.
Queue a mapping
/imports/jobs. The 202 response contains the job’s id.
The job selects a snapshot of existing rows by source.importRowIds,
source.importBatch, or a JSON containment source.filter. Multiple selectors
combine with AND. Rows ingested later are excluded.
Each result starts with an empty entity. Expressions read row, the first
source JSON value, and rows, the list of source JSON values. Use
groupBy: ["accountId"] to combine rows with the same non-null account ID into
one result. Missing/null grouping values stay separate. Groups can contain up
to 1000 rows. Rows within a result are ordered by storage time and ID; select
by source record kind when combining different shapes rather than assuming a
policy or a location comes first.
Jobs run in queue order within a company. Each result records a transformation,
guard and write outcome in an attempt. Mapping, guard and validation failures
are recorded and the job continues. A transformed id updates an entity;
otherwise the job creates one. Use LOOKUP in the id transformation when
rerunning a mapping should update previous results. Requeueing a job without
such an ID can intentionally create additional entities.
Restore historical bound quotes
A queuedQuote job can select quoteMode: "restore-bound" to restore a quote
that was already bound in the legacy system. This mode refuses groupBy.
Each stored source row must retain legacyType: "quotes", legacyId, and the
original record.id, record.status, record.wizardType and record.policyId.
The two quote IDs must agree. The original status must be bound or
applied_to_policy; the latter is accepted only for amending quote types.
Transformations must supply quoteStatus: "bound", matching legacyEntityId
and legacyQuoteStatus, the corresponding canonical quoteType, and a
referencingPolicy that uniquely matches the original policy ID through its
legacyEntityId. The policy must be live in the same company and readable by
the caller. An update must match the stored original quote identity, original
status and type, preserve its submission link, and have no conflicting policy
link or cancelled status. Use LOOKUP for the quote’s id to replay in place.
The import verifies this evidence under transaction locks and verifies the
persisted policy and submission links before committing. Normal field ownership,
validation, audit and parent-submission status recomputation still apply. It
does not execute Bind, create policies or invoices, generate recommended forms,
or issue a new lifecycle form packet. Failed evidence returns
IMPORT_QUOTE_RESTORE_REFUSED in the attempt and leaves the target unchanged.
Ordinary imports and entity updates retain their existing lifecycle guards.
Preserve legacy phone text
Phone fields enforce E.164 by default. When source data contains incomplete numbers, extensions or typos that must be retained, configure that field’stypeInfo as { "kind": "Phone", "args": { "allowUnparsed": true } }.
For a nested value such as payeeDetails.phoneNumber, apply the setting to the
phone subfield in its custom object definition.
Valid numbers still normalize to E.164 and blank values become null. Other
nonblank strings are preserved verbatim; non-string values remain invalid.
This setting applies to every write of that field, including later edits.
PARSE_PHONE(source).value retains unparseable text, and its .warning reports
unparsed-phone; retain that warning in the import attempt for review. After
updating the configuration, requeue the mapping against the existing source rows.
Policy import contracts
A dry run refuses withfield-model-simulation-needs-save when a calculation
requires saving to determine its value, such as allocating a new sequence
number. The error names those fields; it does not allocate numbers or report
unperformed validation as successful. Write mode can execute those calculations
and may succeed for the same input. Invariants requiring save-only operations
also fail explicitly during a dry run.
Use the same /imports/rows and /imports/jobs endpoints. A Policy job requires
entityType: "Policy" and policyMode: "whole-policy" or
"single-transaction". One stored row produces one policy term or one transaction;
groupBy is refused. The job’s transformations construct a command envelope,
using the same expressions, nested { values: ... } objects and mapOver lists
as other imports. Source rows may retain any legacy shape.
Whole policy
The transformed value separatesterm from ordered segments. For example:
term contains all term-invariant fields, including fullTermPricingInfo and
an optional previousPolicy UUID. segments[0].effectiveDate must equal the
term start. Remaining dates strictly increase and stay within the inclusive
term. Each segment’s data is the complete dated policy state, including
embedded exposures. Omitted fields remove earlier values; these are not partial
updates. Every supplied field must be declared and caller-writable in the tenant
configuration. Calculated and system-owned values are refused with field-path
diagnostics; map their stored inputs instead. A term-invariant field repeated
in a segment must equal its value in term, otherwise the attempt fails.
The import creates new business, or renewal when previousPolicy
is present, from the first segment and all term values. Each later segment
becomes an endorsement effective that day, overwriting each dated field through
the term end and removing fields that became empty. Identical adjacent segments
may merge when read back. An optional top-level policyId selects an existing
term for a complete restatement on top of its prior transactions. That
restatement must keep the existing term dates and previousPolicy lineage.
Its first endorsement replaces the whole term before applying later segments.
A cancelled existing policy is reinstated before the restatement; the new
command’s cancellation, if supplied, runs last.
Imported transactions display the author Data Import. Their recording time is
the import time, or the policy’s latest transaction timestamp when that is later;
the segment dates determine when the policy changes take effect.
Cancellation runs last, at cancellation.effectiveDate. Its optional details
are copied into the generated term-invariant cancellationDetails custom-object
field in the first transaction; the final cancel command supplies the date.
Do not also supply term.cancellationDetails when using cancellation.details.
The legacy paymentHandling value is retained as data and never executes an
invoice action.
Cancellation and reinstatement are refused with
IMPORT_POLICY_TAX_SOURCE_REQUIRED when the policy’s resolved rating outputs
contain taxes sourced from InsCipher, or its InsCipher configuration cannot be
interpreted. Policy imports cannot currently source the vendor refund
amounts those changes require. This applies to writes and dry runs; neither
contacts the vendor. A configured InsCipher stage with no vendor tax amounts on
the policy is supported.
Full-term pricing enters through term.fullTermPricingInfo, using the policy
pricing contract’s components, including broker commission components. Pricing
rollups are recomputed by the platform. This import never accepts an invoice
plan and does not migrate legacy transaction history. Transactions still record
the normal earning entries. Existing invoices must satisfy the transaction
pipeline’s billing rules; importing does not bypass those checks.
Single transaction
The transformed command has anaction and that transaction’s inputs:
All five actions execute through the ordinary policy transaction pipeline and
return exactly one committed receipt, or one planned action/date for a dry run.
The
policyId is an FMV1 policy ID; resolve legacy IDs with
LOOKUP('Policy', { legacyEntityId: row.legacyPolicyId }) and guard missing
references before writing. Missing policies are refused; endorsing a cancelled
policy requires reinstating it first. Renewal enforces the existing term-date
and single-successor rules.
All actions accept optional transactionTimestamp and displayAuthor. Explicit
timestamps must not precede the target policy’s latest recording timestamp.
When omitted, the timestamp is the later of import time and the latest recorded
timestamp, and the display author is Data Import. The authenticated audit actor
is retained separately.
Endorsements accept deltas, fullTermPricingInfo, fullTermBillingInfo,
fullTermPolicyRatingResult and crossSegmentRatingOutputs. Cancel and reinstate
accept the three full-term object channels. invoicePlan and
pricingStackLayerChange are outside this import contract. Cancellation details
must already be stored through policy field data; a single cancel does not add
custom field values.
An endorsement applies only its named channels: omitted ordinary policy fields
remain unchanged. Each delta retains its explicit window and action; term and
path invariants still apply. A whole-policy Overwrite explicitly replaces its
object and may remove omitted fields. Supplied calculated/system-owned fields,
including dedicated channel values that would be discarded, are refused with
field-path diagnostics.
Attempts, retries and dry runs
The import records one attempt per source row. A committed PolicywriteResult has status: "ok", entityId, created and ordered transactions
receipts, each with transactionId, policyId, policyVersion, action and
effectiveDate. A result’s policy transactions and receipt commit atomically;
resuming the same worker attempt does not duplicate its writes. Queueing a new
job is an intentional rerun: whole-policy mode writes a complete new set of
transactions, while single-transaction mode attempts the one command again.
For example, a new NEW_BUSINESS job creates another policy, while a repeated
RENEW can be refused by the single-successor guard.
Clearing a Sandbox tenant and importing again is the usual migration workflow.
Both policy modes accept dryRun: true. The import transforms and
validates every planned transaction against the preceding simulated state,
without persisting policy state. Import rows, jobs and attempt diagnostics are
still retained. A successful validation result uses status: "validated",
dryRun: true and planned { action, effectiveDate } entries, with no created
entity or transaction IDs. Validated dry-run results count as successful results
in the job’s writtenResults counter; inspect writeResult.status to distinguish
validation from a committed write.
policy.import authorizes Policy jobs and their import records, including all
supported transaction actions; a transformation that reads policies additionally
needs policy.view. Admin and Manager templates include policy.import.
Existing company roles gain it only when their role templates are synchronized.
Declared Policy field write groups still apply to caller-authored values and
removals, including omitted stored fields removed by a whole-policy replacement.
Imports never attach an invoice plan.
The synchronous Import Entity and Retry Import compatibility endpoints continue
to accept only CRUD entity types.
Progress and reruns
GET/imports/jobs/{jobId} for status and counters, and list attempts with
/imports/attempts?jobId=... for details. Each job attempt includes jobId and
sourceRowIds; importRowId is its first source row. Filtering attempts by
importRowId finds combined results that used that row too.
completed means every result was attempted; check failedResults for refused
results. failed means an infrastructure, configuration or authorization error
stopped the job. Previous attempts remain available. Workers resume interrupted
results without repeating committed writes. A lost dispatch is recovered by
the automatic reconciliation sweep; the job stays queued while waiting.
Queue another job with corrected transformations or guards to rerun any selected
rows, or choose another entity type to produce another entity from the same data.
Permissions
Ingesting and reading untyped rows requires any entity import permission in the company. A job requires its target type’s import permission. Source rows from the synchronous endpoint retain their original entity-type permission checks. Workers retain the submitting identity, recheck import permissions and key revocation, and apply the caller’s normal read permissions toLOOKUP.
Supported target types are Event, Exposure, Quote, Submission, Person
and Organization. Policy jobs use the transaction contracts above, including both execution modes and dry runs.
Legacy activity
A record’s activity in the legacy system is preserved as the legacy system displayed it. After a record is imported, send its whole legacy activity feed to Replace Record Legacy Activity. Each entry carries finished text: the heading, who acted, and each changed line with the label and values already worded, with names in place of legacy ids. The entries are stored as sent and never read through the company’s configuration, so a later configuration change cannot alter them. The entries appear on the record’s Activity tab under Before migration, below the activity recorded since. Entries markedprivileged are shown only
to people who can read privileged notes.
The request replaces the record’s preserved feed, so a rerun is safe. An entry
is held by one record at a time: sending a sourceId to a second record moves
it there. Preserved activity outlives its record: deleting the record, or
clearing a record type’s data in a Sandbox Company, keeps it, and a fresh
import moves each entry to the new record. To confirm nothing
was left behind, add up sourceRowCount across the migrated records and
compare it with the legacy system’s activity row count.
Requires <entityType>.import for the record’s type, and
note.view-privileged to send a privileged entry.
Synchronous endpoints
POST /imports and POST /imports/retry remain available for existing callers.
The sections below describe that compatibility flow and its transformation
syntax. New integrations should use the storage and job operations above.
Retries of job results use a new job.
The request
Any other body key is a
400, as is any query parameter.
Transformations
transformations is an object with one entry per field of the company’s field
model. Each key is the field’s camelCase reference ID. Each value is a JEXL
expression that computes the field from data:
data.<entityKey>:
- Fields without a transformation carry over unchanged. In the example
above,
nameandsourceSystemCodestay on the transformed entity. - Each transformation sets its own field. An expression that returns
nullsets the field tonull. An expression that returns no value, such as a path that does not exist indata, leaves the field as it was carried over. - Expressions read
data, not each other. Every top-level key ofdatais a name an expression can read (submission.name,broker.email), even a key that shares its name with a JEXL root such aspolicy. One transformation cannot read the result of another.
name and
sourceSystemCode in this example, never reach the write. You can still see
them in the attempt’s transformationResult, and the row keeps the whole
original record.
Expressions run on the server in the Import transformation location and
can call any macro that location accepts, including LOOKUP. The
JEXL reference
lists the available roots and macros, and
Author JEXL expressions explains
operators and evaluation.
Mapping a list of child objects
A field that holds a list of objects, such as a list of locations, uses themapOver form. It produces one child object for each element of the list at
the mapOver path:
data.submission.sites of
[{ "region": "CA", "sqft": 12000 }, { "region": "NV", "sqft": 8000 }], this
sets locations to
[{ "state": "CA", "squareFootage": 12000 }, { "state": "NV", "squareFootage": 8000 }].
mapOveris a dotted path intodata, such assubmission.sites. A numeric segment indexes a list (submission.buildings.0.floors). If the path does not resolve to a list, the field fails.- Each key of
valuesis a child key, and each value is a JEXL expression evaluated once per element, or another mapping. A nested mapping’s path reads the current scope, for exampleme.children. Insidevalues:- Bare names (
region) andme(me.sqft) read the current element. parentreads the object that holds the list. For a one-segment path, that object isdataitself.rootreads the wholedata.- The top-level keys of
datastay readable by name (broker.email).
- Bare names (
- When names collide,
me,parentandrootwin over everything else, and an element key shadows a top-leveldatakey of the same name. - An expression that returns no value leaves that key off the child object.
- A
{ "values": { ... } }constructor withoutmapOverbuilds a single object in the surrounding scope. Use it for an object containing a mapped list, such aspayeeDetails.locations. Its expressions keep the surrounding roots; it does not introduce another element. - At every nesting depth,
rootis the originaldata,meis the current element andparentholds the current list. A nested error names its full indexed path and omits the entire top-level field.
PARSE_PHONE(value) returns both value
and warning; an undeclared helper transformation can preserve its warning in
the attempt while being stripped from the entity before writing. LOOKUP_VALUES
maps a dynamic list through the same authorized lookup as LOOKUP, preserving
nulls for unresolved references so a guard can reject a partial join list.
At the top level of transformations, me, parent and root are not
offered. A top-level data key with one of those names is read like any other
key.
When a transformation fails
A transformation fails when its expression does not compile, calls a macro the Import transformation location does not accept, names a root that location does not offer, or raises an error while it runs. A failed field is left off the transformed entity. The other fields still transform, so the attempt reports every failure, not just the first. When any field fails, the guards do not run and nothing is written.field and a code. A failure inside a mapOver also has
a path, such as locations[2].state. If data.<entityKey> is missing or is
not an object, the whole input fails with IMPORT_ENTITY_DATA_MISSING and an
empty field.
Looking up an existing entity
LOOKUP(entityType, filter) returns the ID of an existing entity that you can
identify by its field values but whose ID you do not know. It is available only
in import transformations, including inside mapOver values.
-
entityTypeis one of the searchable types:Event,Exposure,Quote,Submission,Person,OrganizationorPolicy. -
filteris an object that maps field reference IDs of that type to the value each field must equal. Every entry must match, and equality is the only comparison. Each filter value must be a string, number or boolean. A filter cannot be empty. -
Each filter field must hold a single value, not a list, and must be one of
these kinds:
LOOKUP reads this company’s live records that you may see. The result depends
on how many records match:
To view an entity type, your key needs that type’s
view permission, such as
organization.view, in addition to the import permission. LOOKUP fails
rather than returning null for a type you cannot view because a null id
would create a new entity, and a null link would clear the link.
The LOOKUP reference entry
lists its signature and accepted location.
Policy references
Policy lookups search the stored fields in the latest committed transaction
version, regardless of whether the policy term is past, current or future.
Every predicate must name a configured, single-valued term-invariant Policy
field. A per-segment field is rejected because it cannot identify one state for
the whole term. Multiple matching segments of the same policy count as one
match; multiple live policies remain ambiguous. Deleted policies are excluded.
Your key needs policy.view, and the request’s record scope still applies.
LOOKUP_VALUES uses these same rules, including for an empty input list.
For example, import an Event’s policy reference using a migrated identity field:
null, so the guard refuses the write while retaining
the stored source row and attempt. After importing the policy, queue a new job
for that row to resolve the link. The guard does not automatically schedule a
retry. An Event’s selected policy must insure at least one of its selected
exposures. Quote imports can write referencingPolicy and previousPolicy;
amending quotes need their policy on create, and renewal quotes also need a
submission. An unbound renewal uses previousPolicy for its expiring term;
referencingPolicy is reserved for the new term created by binding. Defer complete amending or bound Quote rows until their policy is
available. Bound quotes cannot receive a later generic link update.
Create or update
If the transformed entity has anid, the import updates that entity.
Otherwise, it creates a new one. The id can come from the record itself
(data.<entityKey>.id), from ancillary data, or from a transformation:
LOOKUP on id, a record that matches an existing entity updates it, and
a record that matches nothing creates a new entity, because LOOKUP returns
null. The attempt’s action records which one happened.
Making an import safe to repeat
Import requests take noIdempotency-Key. A well-formed import doesn’t need
one: give the top-level entity an id transformation that LOOKUPs it by a
field that identifies it in your source, such as a tax ID or your own system’s
code. The first request creates the entity, and any later request with the
same record finds it and updates it with the same values. Sending a request
again, or importing a record whose first attempt failed, then leaves one
entity, not two.
If someone edits the entity between the two requests, the second request sets
the fields it carries back to the imported values, undoing those edits. This is
what sending the same update to the
entity endpoints again does.
An update changes only the fields the transformed entity carries, like a
PATCH to the entity. The id names the entity and is not written as a field.
An id that is not a UUID, or that names no entity of that type in this
company, is reported as a failed write in the attempt.
Either way, the write follows the same rules as a create or update through the
entity endpoints, including field
validation, the company’s entity invariants, a bound quote’s immutability and,
for a created quote, its recommended quote forms.
Guards
A guard is a condition that must hold before the entity is written. It has the same shape as one of the company’s entity invariants:- The condition reads the transformed entity’s fields by bare name. It runs
before undeclared keys are removed, so it can read source fields, such as
statushere, that never reach the write. It cannot read ancillary data directly. To guard on ancillary data, transform it into a field first. - A guard holds only when its condition evaluates to exactly
true.false,nulland any other value refuse the import. A condition that cannot be evaluated also refuses it. - Every guard runs, and every guard that does not hold is reported, not just the first one.
- Guards run in addition to the company’s own entity invariants. Those invariants still run during the write.
writeResult is
null, and guardResult lists each guard that did not hold by its index:
reason is refused when the condition returned anything but true, and
error when the condition could not be evaluated. For error, errorMessage
says that the condition could not be checked. Conditions are evaluated as
entity invariants
are. See the JEXL reference for
operators and macros.
Reading an attempt
Import Entity and Retry Import both answer201 with the import row’s ID and
the attempt they recorded. This is the same attempt that
Get Import Attempt returns:
201 means only that the row was stored and an attempt was recorded. To find
out whether the entity was written, check writeResult.
How far an attempt got determines which results are
null:
The entity write and its
writeResult are saved together, in one database
transaction: either both are stored or neither is. So an attempt whose guards
passed and whose writeResult is null did not write the entity. It is still
in progress, or it stopped on an unexpected error before the write was saved.
An attempt the Import Entity or Retry Import response returns is always
finished; one you find later in this state with an updatedAt well in the
past has stopped, and you can retry its row without risk of
writing the entity twice.
A refused write carries the error that the entity create or update endpoint
would have returned: its httpStatus, code, message, userMessages and
any details. For example, a company invariant refusal looks like this:
Retrying a row
Retry Import runs a new attempt on a stored row. Name the row in exactly one of two ways:importRowIdretries the row with thetransformationsandguardsyou send. Each defaults to none, as on an import, so send all the transformations and guards the retry needs, not only the ones you changed.importAttemptIdretries that attempt’s row with the attempt’s own transformations and guards. Use it when the mapping was right but something outside the import has since been fixed, such as the company’s configuration or a record the import refers to. Sendingtransformationsorguardswith animportAttemptIdis a400. To change them, retry byimportRowId.
400. A retry always uses the row’s stored
data and entity type. A retry cannot change the entity type, and the request
takes no entityType.
The new attempt is linked to the same row and carries the retry’s
attemptBatch. No new row is stored. The response has the same shape as an
import’s.
Reviewing a batch
Each import row keeps its request’simportBatch. Each attempt has an
attemptBatch: an import’s attempt takes the request’s importBatch, and a
retry’s attempt takes the retry’s attemptBatch. To review a load, list by
these keys:
List Import Attempts also filters by
entityType and by action. An attempt
whose transformations failed has no action, so it matches neither create nor
update. Filters combine with AND. Both lists are newest first, take 1-based
page and pageSize (default 50; values above 100 are clamped to 100) and
answer { items, totalCount, page, pageSize }. Continue while
page * pageSize < totalCount. An unknown query parameter or filter value is a
400, never an unfiltered page.
Status codes
A transformation failure, a guard refusal or a refused write is never the
request’s status code. Each one is recorded on the attempt, and the request
answers
201. An unexpected server error (500) stops the attempt where it
was: the attempt is kept with the results it reached, and a null
writeResult on it means the entity was not written. If the 500 came after
the row was stored, the row is kept, and you can find it by its importBatch
and retry it.
Permissions
Importing is a verb on each importable entity type’s own permission:event.import, exposure.import, quote.import, submission.import,
person.import, organization.import and policy.import. One type’s permission does not cover
another type.
You read rows and attempts with
import, not view, because a row holds
another system’s source data. A transformation that calls LOOKUP also needs
the view permission of the type it searches. The Admin and Manager role
templates include all seven import permissions.