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

# Property risk engine

> Submit a property address plus its vendor property payload and consume the per-peril evidence envelope and deterministic risk assessment.

The property-risk engine takes a US property address plus the vendor property-data
response for that property, and returns a per-peril evidence envelope with a
deterministic risk assessment. It ships as an `entity_analysis` engine whose
released configuration declares an output contract, so it runs on the platform's
deterministic contract-mode path. No model is involved: `evidence` and
`assessment` are a projection of the upstream flood facts, so the same input and
the same upstream data produce the same `evidence` and `assessment` on every
run. Two envelope values are still per-run by design and are not part of that
guarantee: `request_id` is the run's own ID, and `evidence.flood.vintage` moves
with the fetch date. Do not use whole-output equality as a replay check.

This page is the integration contract for that engine. For the generic run
lifecycle see the [Integration guide](/guides/integration); for the endpoint
schemas see [Submit Run](/api-reference/engines/submit-run) and
[Get Run Status](/api-reference/engines/get-run-status).

<Warning>
  Flood evidence is derived from public FEMA National Flood Hazard Layer data.
  It is informational and is **not** a substitute for a paid flood
  determination. `status: "not_mapped"` does not mean the property has no flood
  risk. Public data can be incomplete or stale. The lender's official flood
  determination controls.
</Warning>

## Flow

```mermaid theme={null}
flowchart LR
  A["Your backend"] -->|"POST /api/v1/engines/{engine_slug}/runs"| B["202 Accepted, run queued"]
  B --> C["Declared-input validation, then enrichment"]
  C -->|"run.completed webhook"| D["data.run.output"]
  C -->|"GET /api/v1/runs/{run_id}"| E["output"]
```

Submission is asynchronous. A `202` means the request envelope was accepted and
a run row exists, not that the payload satisfied the engine's declared input
contract. See [Validation timing](#validation-timing) below.

## Submit a run

```bash theme={null}
curl -X POST "https://api.usenexio.com/api/v1/engines/$ENGINE_SLUG/runs" \
  -H "Authorization: Bearer $NEXIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "subject_address": {
        "line1": "4212 Bayshore Blvd",
        "line2": "Unit 3B",
        "city": "Tampa",
        "state": "FL",
        "zip": "33611"
      },
      "cotality_response": {
        "siteLocation": {
          "data": {
            "propertyId": "100000000001",
            "situsAddress": "4212 BAYSHORE BLVD UNIT 3B, TAMPA, FL 33611"
          }
        },
        "buildings": {
          "data": {
            "clip": "1000000001",
            "yearBuilt": 1998,
            "totalAreaSqFt": 2140
          }
        }
      }
    }
  }'
```

This command is complete and runnable as written: the request above satisfies the
declared input contract, so it produces a real run. The `cotality_response`
shown is a minimal synthetic stand-in. In production, replace it with the
complete vendor response for the subject property, every block exactly as the
vendor returned it. See [Request contract](#request-contract).

```python theme={null}
import httpx

response = httpx.post(
    f"https://api.usenexio.com/api/v1/engines/{engine_slug}/runs",
    headers={"Authorization": f"Bearer {api_key}"},
    json={
        "input": {
            "subject_address": {
                "line1": "4212 Bayshore Blvd",
                "line2": "Unit 3B",
                "city": "Tampa",
                "state": "FL",
                "zip": "33611",
            },
            "cotality_response": cotality_response,
        }
    },
)
response.raise_for_status()
run_id = response.json()["run_id"]
```

```typescript theme={null}
const response = await fetch(
  `https://api.usenexio.com/api/v1/engines/${engineSlug}/runs`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      input: {
        subject_address: {
          line1: '4212 Bayshore Blvd',
          line2: 'Unit 3B',
          city: 'Tampa',
          state: 'FL',
          zip: '33611',
        },
        cotality_response: cotalityResponse,
      },
    }),
  },
)
if (!response.ok) throw new Error(await response.text())
const { run_id: runId } = await response.json()
```

The accepted response is `202`:

```json theme={null}
{
  "run_id": "bcb87157-0bfc-404d-a120-7f5c9cd01037",
  "status": "queued"
}
```

Persist `run_id`. It is the identifier used by polling, webhook payloads,
delivery history, and support.

## Request contract

The request body carries a single `input` object. This engine declares its input
schema in **exclusive** mode: any field that is not listed below, and is not
nested inside `cotality_response`, is rejected.

| Field path                    | Type   | Required | Semantics                                                                                                                                                                                                                                                                                                                                                                              |
| ----------------------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `input.subject_address.line1` | string | yes      | Street line 1. Drives the rooftop-precision geocode that the flood lookup depends on.                                                                                                                                                                                                                                                                                                  |
| `input.subject_address.line2` | string | no       | Unit or suite. Omit it, or send explicit `null`, when there is none.                                                                                                                                                                                                                                                                                                                   |
| `input.subject_address.city`  | string | no       | City name. Not required by the schema, but send it: it materially improves geocode precision, and precision is what determines whether flood evidence can be produced at all.                                                                                                                                                                                                          |
| `input.subject_address.state` | string | no       | Two-letter state code. Same guidance as `city`.                                                                                                                                                                                                                                                                                                                                        |
| `input.subject_address.zip`   | string | yes      | Five-digit US ZIP. ZIP+4 is accepted; the flood lookup verifies against the five-digit prefix. Must match `^\d{5}(-\d{4})?$`.                                                                                                                                                                                                                                                          |
| `input.cotality_response`     | object | yes      | The complete vendor property-API response for the subject property, every block exactly as returned. Opaque to the engine's field schema: no specific block is required and no nested field is type-checked. It must be a non-empty object. Stored with the run. Platform request bounds still apply to its nested structure, see [Limits and payload size](#limits-and-payload-size). |

Required-in-practice versus transport-optional: only `line1`, `zip`, and
`cotality_response` are enforced. `city` and `state` are declared optional so a
partial address still produces a run rather than a hard failure, but omitting
them raises the odds of a below-rooftop geocode and therefore of
`evidence.flood.status: "unavailable"`.

`cotality_response` is passed through verbatim and its shape is defined by the
vendor, not by Nexio. Any non-empty object is a valid submission, which is why
the runnable example in [Submit a run](#submit-a-run) works with a synthetic
stand-in. In production, send the complete vendor response, every block exactly
as returned: the contract requires the full payload to be stored with the run,
and there is no canonical Nexio-side schema for it to conform to beyond
non-emptiness and the platform request bounds.

One transport constraint applies: the payload is decoded and re-encoded as
standard JSON on intake and storage, so it is preserved semantically, not byte
for byte. String and boolean values keep their exact content. Numbers are
parsed as IEEE 754 doubles: integer magnitudes beyond 2^53 can be silently
altered (9007199254740993 stores as 9007199254740992), and lexical forms are
normalized (1.50 becomes 1.5, 1e2 becomes 100). The vendor serves opaque
identifiers (`propertyId`, `clip`) as strings, so typical responses are
unaffected. Any value whose exact original text must survive should be
transmitted as a JSON string.

### Validation timing

Envelope-level checks run synchronously at submission and reject before a run is
created: malformed JSON, unknown top-level request fields, missing `input`,
request bounds, authentication, capability, rate limit, and monthly cap.

The **declared input contract above is validated asynchronously**, after the
`202`. A submission that clears the envelope but violates the declared schema
creates a run that transitions to terminal status `failed`, and delivers a
`run.failed` webhook. Read `error` on the run for the reason. Typical messages:

| Condition                                                     | `error`                                                                                              |
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Required field missing, `null`, blank string, or empty object | `required input subject_address.line1 is missing or empty`                                           |
| Declared field has the wrong JSON type                        | `input <path> must be a <declared type>`, for example `input subject_address.line1 must be a string` |
| A path that contains declared fields is not an object         | `input subject_address must be an object`                                                            |
| `zip` fails the format pattern                                | `input subject_address.zip does not match the required format`                                       |
| Undeclared field present in `input`                           | `input subject_address.county is not part of this engine's request contract`                         |

`failed` is not always a request defect, so classify it from `error` before
deciding what to do:

* `error` names an input path (`required input ...`, `input ... must be ...`,
  `input ... does not match ...`, `input ... is not part of this engine's
  request contract`): a request defect. Fix the payload. Re-submitting it
  unchanged fails identically.
* Any other `error`: not a caller defect. Retryable internal failures are
  retried by the worker before the run goes terminal, so a terminal `failed`
  here has already used that budget (`attempt` on the run tells you how many
  executions it took). Re-submitting the same payload once is safe, and if it
  fails again, report the `run_id` and `trace_id` to `support@usenexio.com`
  rather than looping.

## Response envelope

The output is delivered in two places, identically:

* `data.run.output` on the `run.completed` webhook, when the endpoint's
  `payload_mode` is `full`
* `output` on `GET /api/v1/runs/{run_id}`

A completed run for a property inside a Special Flood Hazard Area:

```json theme={null}
{
  "response_type": "property_risk_assessment",
  "request_id": "bcb87157-0bfc-404d-a120-7f5c9cd01037",
  "evidence": {
    "flood": {
      "status": "complete",
      "source": "FEMA_NFHL",
      "vintage": "2026-07-24",
      "confidence": "high",
      "zone_code": "AE",
      "in_sfha": true,
      "zone_description": "Special Flood Hazard Area subject to the 1% annual-chance flood."
    },
    "wildfire": null,
    "coastal_distance": null,
    "earthquake": null,
    "fire_protection": null
  },
  "assessment": {
    "material_risks": [
      {
        "peril": "flood",
        "summary": "The property maps to FEMA Zone AE, within the Special Flood Hazard Area.",
        "evidence_refs": ["flood"]
      }
    ],
    "narrative": "FEMA maps place this property in Zone AE, within the Special Flood Hazard Area (a 1% or greater annual chance of flooding). This is an informational risk read, not an official flood determination.",
    "producer_note": "Zone AE/SFHA signal present. Confirm the lender's official flood determination and discuss appropriate flood coverage."
  }
}
```

### Top level

| Field           | Type          | Always present | Semantics                                                                                                             |
| --------------- | ------------- | -------------- | --------------------------------------------------------------------------------------------------------------------- |
| `response_type` | string        | yes            | Constant `property_risk_assessment` for this engine. Branch on it if you consume several engines through one handler. |
| `request_id`    | string (uuid) | yes            | The run ID. Identical to the `run_id` returned at submission and to `data.run.run_id` on the webhook.                 |
| `evidence`      | object        | yes            | Per-peril evidence. Every peril key is always present.                                                                |
| `assessment`    | object        | yes            | Derived reads templated from the evidence.                                                                            |

### `evidence.flood`

| Field              | Type                  | Present when                                 | Semantics                                                                                                                                                      |
| ------------------ | --------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`           | string                | always                                       | One of `complete`, `not_mapped`, `unavailable`. See below.                                                                                                     |
| `in_sfha`          | boolean or null       | always                                       | FEMA's authoritative Special Flood Hazard Area flag. Explicit `null` when FEMA supplies no definitive value, which is not the same as `false`.                 |
| `source`           | string                | always                                       | Constant `FEMA_NFHL`.                                                                                                                                          |
| `vintage`          | string (`YYYY-MM-DD`) | when an upstream fetch time exists           | See [vintage](#vintage).                                                                                                                                       |
| `zone_code`        | string                | `status: "complete"`                         | FEMA flood zone code, for example `AE`, `X`, `A99`, `VE`.                                                                                                      |
| `zone_description` | string                | `status: "complete"`, when FEMA supplies one | Plain-language description of the zone. A complete block can omit it.                                                                                          |
| `confidence`       | string                | `status: "complete"`                         | Match confidence for the geocode behind the lookup. `high` is the only value emitted, because rooftop precision is the floor for producing flood facts at all. |

### Flood statuses

Branch on `status`, never on field presence.

| `status`      | What happened                                                                                                                               | What to do                                                                                                                                                                                                                                                                                  |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `complete`    | The address resolved to rooftop precision and FEMA returned an intersecting flood-layer feature. `zone_code` and `in_sfha` carry the facts. | Consume the evidence and the assessment. Remember `in_sfha: null` means unknown, never "outside the SFHA".                                                                                                                                                                                  |
| `not_mapped`  | The lookup succeeded and FEMA returned no intersecting flood-layer feature for the point.                                                   | Record that FEMA has no mapped feature at this address. Do **not** interpret it as no flood risk. Do not retry: against the same FEMA map the answer is the same. Treat the result as dated rather than permanent, and re-run on your own refresh cadence, since FEMA republishes its maps. |
| `unavailable` | No flood fact could be produced on this run. Two distinct causes, see below.                                                                | Follow the retry rule below. Blind retrying is wrong for one of the two causes.                                                                                                                                                                                                             |

`unavailable` covers two causes that call for opposite responses:

* **Address precision.** The geocoder resolved the address below rooftop
  precision (interpolated, geometric center, approximate, or unknown), so the
  FEMA call was never made. Rooftop precision is a hard floor for flood-zone
  determination. Re-submitting the same address will not help.
* **Transient upstream degradation.** The geocoding or FEMA call failed, timed
  out, was cut off by a budget or circuit breaker, or returned data the engine
  refused to serve as complete. A later run of the same address can succeed.

The webhook payload does not label which one occurred. The run read does: this
engine exposes a `warnings` array on `GET /api/v1/runs/{run_id}`, and on an
`unavailable` result it carries the machine-readable cause. Poll the run once
after an `unavailable` webhook and branch on it:

```json theme={null}
{
  "warnings": [
    {
      "source": "fema_nfhl",
      "stage": "contract_mode_enrich",
      "error_class": "enrichment_precision_below_floor",
      "retryable": false,
      "at": "2026-07-27T14:31:39Z"
    }
  ]
}
```

* `retryable: false` (for example `enrichment_precision_below_floor` or
  `malformed_field`): re-submitting the same address will never help. Fix the
  address. If `city` or `state` was omitted, or line 1 is a PO box, a rural
  route, an intersection, a lot or parcel description, or new construction the
  geocoder may not have indexed, that is the cause under your control.
* `retryable: true` (for example `enrichment_degraded` or
  `enrichment_degraded_circuit_open`): the upstream lookup failed transiently.
  Retry the same address after a delay; a later run can succeed.

`warnings` appears only on the run read, never in the webhook payload, and only
on runs where a non-informational warning was recorded. If you consume webhooks
without polling, fall back to this triage order: inspect the address first (the
cause you control), retry a complete deliverable address at most once after a
short delay, and after a second `unavailable` stop retrying in line. Record the
run as carrying no flood signal, and re-run later or send the `run_id` to
`support@usenexio.com` if you need the specific cause.

Whatever the cause, `unavailable` is a valid terminal result, not an error.
Persist the run and the absence of a flood signal rather than dropping it.

Enrichment failure never fails or degrades the run. A run whose flood lookup
degrades still reaches terminal status `completed` and still delivers a
`run.completed` webhook, with `evidence.flood.status` set to `unavailable` and
an `assessment.narrative` that says the read could not be completed. Do not
treat a missing flood fact as a delivery problem.

### Terminal statuses

The platform-wide run contract defines three terminal statuses: `completed`,
`degraded`, and `failed`. This engine reaches only two of them.

| Status      | Reachable here | Meaning                                                                                                                                                                                          |
| ----------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `completed` | yes            | The envelope was composed. Read `evidence.flood.status` to learn whether a flood fact was produced.                                                                                              |
| `failed`    | yes            | The submission violated the declared input contract, or the run hit an internal error. Read `error`.                                                                                             |
| `degraded`  | no             | Contract-mode engines never enter `degraded`, so `output.degradation_reason` is never present on this engine's output. Degradation is expressed inside the envelope, on `evidence.flood.status`. |

Keep `degraded` in your terminal-status switch anyway: it is part of the
platform contract, it is terminal, and treating any terminal status as
non-terminal turns a poll into an infinite loop.

### Reserved peril blocks

`evidence.wildfire`, `evidence.coastal_distance`, `evidence.earthquake`, and
`evidence.fire_protection` are always present and always `null` in this version.

They are typed **object or null** in the engine's published contract, not
`null`. Later phases populate them as objects without a breaking schema change
on your side. Bind to the full envelope now: parse the keys, tolerate `null`,
and do not fail closed on an unexpected object appearing in one of them.

### `vintage`

`vintage` is the UTC calendar date (`YYYY-MM-DD`) on which the flood data was
fetched from the upstream FEMA source. A cached result replayed on a later run
keeps its original fetch date, so `vintage` can be older than the run.

`vintage` is **not** the FIRM panel effective date and not the date FEMA
published the map. It says when Nexio read the data, not how old the data is. It
is absent when no upstream fetch time exists for the run.

### `assessment`

| Field                             | Type                           | Semantics                                                                                                                                                   |
| --------------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `material_risks`                  | array                          | Material findings, highest materiality first. Empty array means no material risk surfaced, never `null`. Only the `flood` peril is emitted in this version. |
| `material_risks[*].peril`         | string                         | Peril key, matching an `evidence` key.                                                                                                                      |
| `material_risks[*].summary`       | string                         | One-sentence finding.                                                                                                                                       |
| `material_risks[*].evidence_refs` | array of string                | Keys under `evidence` that support the finding.                                                                                                             |
| `narrative`                       | string, at most 500 characters | Consumer-facing read of the evidence.                                                                                                                       |
| `producer_note`                   | string, at most 500 characters | Producer-facing next action.                                                                                                                                |

Both prose fields are templated projections of the evidence, not model output.
Every narrative carries the informational-not-a-determination framing; do not
strip it when you surface the text.

## Limits and payload size

The vendor property payload is usually what drives request size, so size the
integration against the request bounds rather than assuming a small body.

* The HTTP request envelope limit is fixed at the platform ceiling and a
  violation returns `413`.
* This engine's released configuration raises every configurable JSON bound
  (canonical bytes, string length, array items, object fields, depth) to the
  maximum configurable ceiling in the platform table. These bounds are structural
  and apply to the whole `input` tree, including everything nested inside
  `cotality_response`, even though no field in there is type-checked.
* Bound violations are deterministic, are returned before the run is created,
  and carry `code: "request_bound_exceeded"` with `details.bound`,
  `details.path`, `details.limit`, and `details.actual`.

The exact ceilings and the error shape are in
[Limits and cost controls](/reference/limits).

Run submission shares the organization's public-API per-minute window. A
rejected request returns `429` with `code: "rate_limited"` and a `Retry-After`
header in seconds. Honor the header; do not retry in a tight loop. Monthly run
caps and escalation are on the same page.

## Delivery and reconciliation

Terminal results arrive by webhook. `run.completed` carries the output; any run
that reached terminal `failed`, from a declared-input violation or from an
internal error, arrives as `run.failed`.

Deliveries are at-least-once and unordered, so verify the signature over the
raw bytes and deduplicate on `X-Nexio-Delivery` before processing. Retry
schedules, dead-letter behavior, and resend are documented in
[Webhook verification and delivery](/api-reference/webhooks/overview).

Webhooks are notifications, not the source of truth. After a missed, duplicated,
delayed, or out-of-order callback, reconcile with
`GET /api/v1/runs/{run_id}` using the `run_id` you persisted at submission.

## Integration checklist

1. Persist `run_id` at submission and reconcile by polling on any webhook gap.
2. Treat `completed`, `degraded`, and `failed` as terminal, even though this
   engine only ever reaches `completed` and `failed`.
3. Branch on `evidence.flood.status`, never on field presence.
4. Treat `in_sfha: null` as unknown, never as `false`.
5. On `unavailable`, check the submitted address before retrying: correct a
   partial or non-deliverable address, retry a complete one at most once, then
   stop.
6. Parse all five `evidence` peril keys and tolerate `null` today and an object
   later.
7. On terminal `failed`, classify from `error`: an input-path message is a
   request defect to fix, anything else is platform-side.
8. Surface `assessment.narrative` with its informational-not-a-determination
   framing intact.
