Skip to main content
The Import API separates storage from mapping. First store arbitrary source JSON. Then queue jobs that choose an entity type, transformations, guards and source rows. Mapping failures never prevent ingestion. The same row can feed several entities, and a new mapping can run over old rows without re-importing them.

Store rows

POST this to /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

POST this to /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 queued Quote 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’s typeInfo 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

Whole-policy and single-transaction imports support execution and dry runs. The save-only calculation and vendor-tax restrictions below apply to both modes.
A dry run refuses with field-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 separates term 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 an action 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 Policy writeResult 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 to LOOKUP. 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 marked privileged 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:
The transformed entity starts as a copy of data.<entityKey>:
  • Fields without a transformation carry over unchanged. In the example above, name and sourceSystemCode stay on the transformed entity.
  • Each transformation sets its own field. An expression that returns null sets the field to null. An expression that returns no value, such as a path that does not exist in data, leaves the field as it was carried over.
  • Expressions read data, not each other. Every top-level key of data is a name an expression can read (submission.name, broker.email), even a key that shares its name with a JEXL root such as policy. One transformation cannot read the result of another.
Before the entity is written, every key that the company’s field model does not declare is removed. Leftovers from the source shape, such as 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 the mapOver form. It produces one child object for each element of the list at the mapOver path:
Given data.submission.sites of [{ "region": "CA", "sqft": 12000 }, { "region": "NV", "sqft": 8000 }], this sets locations to [{ "state": "CA", "squareFootage": 12000 }, { "state": "NV", "squareFootage": 8000 }].
  • mapOver is a dotted path into data, such as submission.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 values is 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 example me.children. Inside values:
    • Bare names (region) and me (me.sqft) read the current element.
    • parent reads the object that holds the list. For a one-segment path, that object is data itself.
    • root reads the whole data.
    • The top-level keys of data stay readable by name (broker.email).
  • When names collide, me, parent and root win over everything else, and an element key shadows a top-level data key of the same name.
  • An expression that returns no value leaves that key off the child object.
  • A { "values": { ... } } constructor without mapOver builds a single object in the surrounding scope. Use it for an object containing a mapped list, such as payeeDetails.locations. Its expressions keep the surrounding roots; it does not introduce another element.
  • At every nesting depth, root is the original data, me is the current element and parent holds the current list. A nested error names its full indexed path and omits the entire top-level field.
The JEXL reference also lists strict conversion functions for text, numbers, scaling, calendar dates, entered addresses and option lists. 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.
Each error names its 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.
  • entityType is one of the searchable types: Event, Exposure, Quote, Submission, Person, Organization or Policy.
  • filter is 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:
A missing policy produces 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 an id, 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:
With 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 no Idempotency-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 status here, 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, null and 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.
When a guard refuses, nothing is written, the attempt’s 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 answer 201 with the import row’s ID and the attempt they recorded. This is the same attempt that Get Import Attempt returns:
A 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:
  • importRowId retries the row with the transformations and guards you 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.
  • importAttemptId retries 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. Sending transformations or guards with an importAttemptId is a 400. To change them, retry by importRowId.
Sending both IDs, or neither, is a 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.
A retry runs the whole create-or-update again. If the row has already created its entity, a retry without an id creates another entity. To update the entity the row already created instead, set id, for example with LOOKUP.

Reviewing a batch

Each import row keeps its request’s importBatch. 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.