> ## Documentation Index
> Fetch the complete documentation index at: https://docs.go.aiinsurance.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Overview

> Store source rows once, then queue independent transformation jobs

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.

| Method | Path | Reference |
| - | - | - |
| `POST` | `/api/v1/companies/{companyId}/imports/rows` | [Import Rows](https://docs.go.aiinsurance.io/api-reference/imports/import-rows.md) |
| `POST` | `/api/v1/companies/{companyId}/imports/jobs` | [Queue Import Job](https://docs.go.aiinsurance.io/api-reference/imports/queue-import-job.md) |
| `GET` | `/api/v1/companies/{companyId}/imports/jobs/{jobId}` | [Get Import Job](https://docs.go.aiinsurance.io/api-reference/imports/get-import-job.md) |
| `GET` | `/api/v1/companies/{companyId}/imports/rows` | [List Import Rows](https://docs.go.aiinsurance.io/api-reference/imports/list-import-rows.md) |
| `GET` | `/api/v1/companies/{companyId}/imports/rows/{importRowId}` | [Get Import Row](https://docs.go.aiinsurance.io/api-reference/imports/get-import-row.md) |
| `GET` | `/api/v1/companies/{companyId}/imports/attempts` | [List Import Attempts](https://docs.go.aiinsurance.io/api-reference/imports/list-import-attempts.md) |
| `GET` | `/api/v1/companies/{companyId}/imports/attempts/{importAttemptId}` | [Get Import Attempt](https://docs.go.aiinsurance.io/api-reference/imports/get-import-attempt.md) |

## Store rows

```json theme={null}
{
  "importBatch": "october-load",
  "rows": [
    { "sourceId": "A-1", "name": "Acme" },
    { "sourceId": "A-2", "name": "Other" }
  ]
}
```

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

```json theme={null}
{
  "entityType": "Exposure",
  "source": { "importBatch": "october-load" },
  "transformations": {
    "exposureName": "row.name",
    "exposureType": "'company'"
  },
  "guards": [
    { "condition": "exposureName != null", "errorMessage": "A name is required" }
  ],
  "attemptBatch": "mapping-v1"
}
```

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

> [!WARNING]
>
> 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:

```json theme={null}
{
  "term": {
    "policyStartDate": { "date": "2026-01-01", "timezone": "America/Detroit" },
    "policyEndDate": { "date": "2026-12-31", "timezone": "America/Detroit" },
    "policyNumber": "LEGACY-1001",
    "legacyEntityId": "legacy-policy-1001"
  },
  "segments": [
    { "effectiveDate": "2026-01-01", "data": { "exposures": [] } },
    { "effectiveDate": "2026-06-01", "data": { "exposures": [], "coverageLimit": 2000000 } }
  ],
  "cancellation": {
    "effectiveDate": "2026-11-01",
    "details": {
      "reason": "Insured request",
      "explanation": "Business closed",
      "paymentHandling": "do-not-track",
      "amountPaidToDate": 1000
    }
  }
}
```

`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:

| Action | Required inputs |
| - | - |
| `NEW_BUSINESS` | `data`, including both policy dates; `previousPolicy` is refused |
| `RENEW` | `data`, including both policy dates and `previousPolicy` |
| `ENDORSE` | `policyId`, `effectiveDate`, and at least one transaction channel |
| `CANCEL` | `policyId`, `cancellationDate` |
| `REINSTATE` | `policyId`, `reinstatementDate` |

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](https://docs.go.aiinsurance.io/api-reference/imports/replace-record-legacy-activity.md).
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

```json theme={null}
{
  "entityType": "Submission",
  "data": {
    "submission": {
      "name": "Acme Logistics",
      "sourceSystemCode": "SUB-1042"
    },
    "broker": { "email": "broker@example.com" }
  },
  "transformations": {
    "submissionName": "submission.name",
    "brokerEmail": "broker.email"
  },
  "guards": [
    { "condition": "submissionName != null", "errorMessage": "A submission needs a name." }
  ],
  "importBatch": "october-load"
}
```

| Key | Meaning |
| - | - |
| `entityType` | The entity type to import, in PascalCase (`Submission`, not `submission`). Required. |
| `data` | An object holding the source record. Required. The entity goes under its **entity key**, which is the entity type in camelCase: `data.submission` for a Submission, `data.event` for an Event. Every other top-level key, such as `broker` above, is **ancillary data**. Transformations can read ancillary data, but it is never written to the entity. |
| `transformations` | How the entity becomes an entity in this company's field model. Optional. When omitted, no field is transformed. See [Transformations](#transformations). |
| `guards` | Conditions that must hold before the entity is written. Optional. When omitted, there are no guards. See [Guards](#guards). |
| `importBatch` | Your own key for grouping requests, such as the name of a load, so you can [review them later](#reviewing-a-batch). Optional. Omit it or send `null` for none. A blank key is refused. |

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`:

```json theme={null}
"transformations": {
  "submissionName": "submission.name",
  "brokerEmail": "broker.email"
}
```

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`](#looking-up-an-existing-entity). The
[JEXL reference](https://docs.go.aiinsurance.io/api-reference/configuration/jexl-reference.md#expression-locations)
lists the available roots and macros, and
[Author JEXL expressions](https://docs.go.aiinsurance.io/api-reference/configuration/jexl-authoring.md) 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:

```json theme={null}
"transformations": {
  "locations": {
    "mapOver": "submission.sites",
    "values": {
      "state": "region",
      "squareFootage": "me.sqft"
    }
  }
}
```

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](https://docs.go.aiinsurance.io/api-reference/configuration/jexl-reference.md) 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.

```json theme={null}
"transformationResult": {
  "status": "failed",
  "entity": { "name": "Acme Logistics", "site": { "region": "CA" } },
  "errors": [
    {
      "field": "locations",
      "code": "IMPORT_MAP_OVER_NOT_A_LIST",
      "message": "mapOver path 'submission.site' must resolve to a list in data; found an object"
    }
  ]
}
```

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.

```json theme={null}
"transformations": {
  "employer": "LOOKUP('Organization', { taxId: source.employerTaxId })"
}
```

* `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:

  | Field kind | Filter value | Comparison |
  | - | - | - |
  | Text, Phone, URL, Email | String | Equal, ignoring case |
  | StringOrNumber | String or number | Equal as text, ignoring case |
  | Number | Number | Equal |
  | Boolean | `true` or `false` | Equal |
  | Date | ISO day, `YYYY-MM-DD` | Same day |
  | OptionSet | Option key | Same option |
  | Join | Entity ID | Linked to that entity |

`LOOKUP` reads this company's live records that you may see. The result depends
on how many records match:

| Outcome | Result |
| - | - |
| Exactly one entity matches | Its ID. |
| No entity matches | `null`. |
| More than one entity matches | The field fails with `IMPORT_LOOKUP_AMBIGUOUS`. `LOOKUP` never picks one. Retry with a tighter filter. |
| Your API key cannot view that entity type | The field fails with `IMPORT_LOOKUP_FORBIDDEN`. It does not return `null`. |
| An unknown type or field, a list field, an unsupported kind, or a value of the wrong type | The field fails with `IMPORT_LOOKUP_INVALID`. |

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](https://docs.go.aiinsurance.io/api-reference/configuration/jexl-reference#lookup)
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:

```json theme={null}
{
  "transformations": {
    "referencingPolicy": "LOOKUP('Policy', { legacyEntityId: row.legacyPolicyId })"
  },
  "guards": [
    {
      "condition": "IS_PRESENT(referencingPolicy)",
      "errorMessage": "Policy reference is unresolved"
    }
  ]
}
```

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:

```json Update by a known id theme={null}
{
  "entityType": "Submission",
  "data": {
    "submission": { "name": "Acme Logistics LLC" },
    "source": { "fmv1Id": "6e2b8f14-3a7c-4d9e-b051-2c8a9f7d4e13" }
  },
  "transformations": {
    "id": "source.fmv1Id",
    "submissionName": "submission.name"
  },
  "importBatch": "october-renames"
}
```

```json Update the entity LOOKUP finds theme={null}
{
  "entityType": "Organization",
  "data": {
    "organization": { "name": "Acme Logistics LLC", "taxId": "12-3456789" }
  },
  "transformations": {
    "id": "LOOKUP('Organization', { taxId: organization.taxId })"
  },
  "importBatch": "october-renames"
}
```

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 `LOOKUP`s 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](https://docs.go.aiinsurance.io/api-reference/entities/overview.md) 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](https://docs.go.aiinsurance.io/api-reference/entities/overview.md), 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:

```json theme={null}
"guards": [
  { "condition": "status != 'declined'", "errorMessage": "Declined submissions are not imported." }
]
```

* 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:

```json theme={null}
"guardResult": {
  "passed": false,
  "failures": [
    { "index": 0, "errorMessage": "Declined submissions are not imported.", "reason": "refused" }
  ]
}
```

`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](https://docs.go.aiinsurance.io/api-reference/configuration/jexl-authoring.md#author-an-entity-invariant)
are. See the [JEXL reference](https://docs.go.aiinsurance.io/api-reference/configuration/jexl-reference.md) 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](https://docs.go.aiinsurance.io/api-reference/imports/get-import-attempt.md) returns:

```json theme={null}
{
  "importRowId": "9a1d4e7b-2c3f-4b8a-8d61-0e5f2a7b3c94",
  "importAttempt": {
    "id": "3f6c2a1e-8b4d-4c7a-9e21-5d0b7a9c1f42",
    "importRowId": "9a1d4e7b-2c3f-4b8a-8d61-0e5f2a7b3c94",
    "entityType": "Submission",
    "action": "create",
    "attemptBatch": "october-load",
    "transformations": { "submissionName": "submission.name" },
    "guards": [],
    "transformationResult": {
      "status": "ok",
      "entity": {
        "name": "Acme Logistics",
        "sourceSystemCode": "SUB-1042",
        "submissionName": "Acme Logistics"
      }
    },
    "guardResult": { "passed": true },
    "writeResult": {
      "status": "ok",
      "entityId": "6e2b8f14-3a7c-4d9e-b051-2c8a9f7d4e13",
      "created": true
    },
    "createdAt": "2026-10-23T14:58:02.087Z",
    "updatedAt": "2026-10-23T14:58:02.471Z"
  }
}
```

A `201` means only that the row was stored and an attempt was recorded. To find
out whether the entity was written, check `writeResult`.

| Field | Meaning |
| - | - |
| `transformationResult` | `status: "ok"` with the transformed `entity`, or `status: "failed"` with a partial `entity` and one entry in `errors` per failure. The entity is shown before undeclared keys are removed. |
| `action` | `update` when the transformed entity had an `id`, otherwise `create`. `null` when the transformations failed or have not run yet. |
| `guardResult` | `{ "passed": true }`, or `passed: false` with the `failures`. `null` when the guards did not run or have not run yet. |
| `writeResult` | `status: "ok"` with the `entityId` written and `created` (`false` for an update). `status: "failed"` when the write was refused. `null` when no write was attempted, or the write has not finished. |
| `createdAt` | When the attempt was stored, before its first step ran. |
| `updatedAt` | When a step's result was last saved on the attempt. |

How far an attempt got determines which results are `null`:

| Outcome | `transformationResult` | `action` | `guardResult` | `writeResult` |
| - | - | - | - | - |
| In progress, or crashed, before the transformations finished | `null` | `null` | `null` | `null` |
| A transformation failed | `status: "failed"` | `null` | `null` | `null` |
| In progress, or crashed, before the guards finished | `status: "ok"` | `create` or `update` | `null` | `null` |
| A guard refused | `status: "ok"` | `create` or `update` | `passed: false` | `null` |
| In progress, or crashed, before the write finished | `status: "ok"` | `create` or `update` | `passed: true` | `null` |
| The write was refused | `status: "ok"` | `create` or `update` | `passed: true` | `status: "failed"` |
| The entity was written | `status: "ok"` | `create` or `update` | `passed: true` | `status: "ok"` |

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](#retrying-a-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:

```json theme={null}
"writeResult": {
  "status": "failed",
  "httpStatus": 400,
  "code": "TenantInvariantViolation",
  "message": "fieldModelV1Data: The expiration date must be after the effective date.",
  "userMessages": ["The expiration date must be after the effective date."],
  "details": [
    { "field": "fieldModelV1Data", "message": "The expiration date must be after the effective date." }
  ]
}
```

***

## Retrying a row

[Retry Import](https://docs.go.aiinsurance.io/api-reference/imports/retry-import.md) runs a new attempt on a
stored row. Name the row in exactly one of two ways:

```json By row, with new transformations theme={null}
{
  "importRowId": "9c5e1a3d-7f2b-4d8e-a6c4-1b9f3e7d2a58",
  "transformations": {
    "locations": {
      "mapOver": "submission.sites",
      "values": { "state": "region" }
    }
  },
  "attemptBatch": "october-fix"
}
```

```json By attempt, as it ran theme={null}
{
  "importAttemptId": "2d6b9f3a-8e1c-4a7d-b2e9-6f4c1a8d3e57",
  "attemptBatch": "october-rerun"
}
```

* **`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.

> [!WARNING]
>
> 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:

| To find | Request |
| - | - |
| Every row a load stored | `GET .../imports/rows?importBatch=october-load` |
| Every attempt on a load's rows, including later retries under any `attemptBatch` | `GET .../imports/attempts?importBatch=october-load` |
| Only the attempts that one run or retry pass recorded | `GET .../imports/attempts?attemptBatch=october-fix` |
| Every attempt on one row | `GET .../imports/attempts?importRowId=<id>` |

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

| Status | When |
| - | - |
| `201` | Import Entity stored the row, or Retry Import found the row, and an attempt was recorded. This is the answer whether or not the entity was written: check the attempt's `writeResult`. |
| `400` | The request is malformed, and nothing was stored or recorded. Examples: an entity type that cannot be imported (`INVALID_ENTITY_TYPE`, which includes `Policy`), `data` that is not an object, a transformation or guard of the wrong shape, a blank batch key, an unknown body key or any query parameter. On a retry: both IDs or neither, `transformations` or `guards` with an `importAttemptId`, or an ID that is not a UUID. |
| `403` | The key lacks the permission the operation requires. See [Permissions](#permissions). Nothing is stored. |
| `404` | On a retry or a get: the row or attempt does not exist, belongs to another company, or is of an entity type your key cannot import. All three cases look the same. |

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.

| Operation | Requires |
| - | - |
| Import Entity | `<entityType>.import` for the request's `entityType`. |
| Retry Import | `<entityType>.import` for the stored row's entity type. A key with no import permission at all gets a `403`. A row or attempt of a type the key cannot import is a `404`. |
| List Import Rows, List Import Attempts | Any one import permission. The list shows only the types the key can import, and `totalCount` counts only those. An `entityType` filter for a type the key cannot import is a `403`. |
| Get Import Row, Get Import Attempt | Any one import permission. A row or attempt of a type the key cannot import is a `404`. |

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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.