Skip to main content
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; for the endpoint schemas see Submit Run and Get Run Status.
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.

Flow

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 below.

Submit a run

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.
The accepted response is 202:
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. 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 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: 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:

Top level

evidence.flood

Flood statuses

Branch on status, never on field presence. 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:
  • 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. 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

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