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

# Integration Guide

> How to structure provider offerings, handle multi-provider input, and consume results.

This guide covers what the [API reference](/api-reference/engines/submit-run) doesn't: how to structure
your provider offerings for Nexio and interpret the results.

For request/response schemas, see [Submit Run](/api-reference/engines/submit-run) and
[Get Run Status](/api-reference/engines/get-run-status).

## Flow

```mermaid theme={null}
flowchart LR
  A["Your backend"] -->|"POST /api/v1/engines/{engine_slug}/runs"| B["Nexio queues run"]
  B -->|"poll GET /api/v1/runs/{run_id}"| C["Ranked solutions"]
  B -->|"terminal event"| D["Signed webhook"]
```

Persist `run_id`. Polling remains the canonical reconciliation path even when using
[webhooks](/api-reference/webhooks/overview).

## What you send

```json theme={null}
{
  "input": { ... },
  "offerings": [ ... ]
}
```

`input` is the subject's profile and run context. `offerings` is one entry per provider per requirement category.

<Note>
  `input.current_coverage` describes existing state (e.g., incumbent products or services). It provides context for scoring but does
  not replace the `offerings` array.
</Note>

## Building offerings from your data

Each provider entry becomes one offering per requirement category. If you have 3 providers each
covering 3 categories, that's 9 offerings (3 providers x 3 categories).

Keep `offerings[*].category` aligned with `input.coverage_types`. Extra offerings for categories
not in `coverage_types` are ignored.

## Multi-provider input

When comparing packages from multiple providers, submit offerings from each. The engine evaluates
all valid combinations: both single-provider bundles and mixed-provider packages:

<Note>
  **Example: Insurance: Personal Lines.** The `input` and `offerings` schema depends on your engine's configuration. This example shows a personal-lines insurance placement engine.
</Note>

```json theme={null}
{
  "input": {
    "address": { "state": "NY", "zip_code": "11231" },
    "coverage_types": ["home", "auto", "umbrella"],
    "appetite_bucket": "balanced",
    "vehicles": 1,
    "premium": 3557.23
  },
  "offerings": [
    {
      "id": "quote_line_carrier_a_home_001",
      "provider_id": "prov_carrier_a",
      "provider_name": "Carrier A",
      "category": "home",
      "quality_rating": "A+",
      "pricing_tier": "premium",
      "commission": 0.14,
      "constraints": { "excluded_states": [], "prohibited_naics": [], "avoided_naics": [] },
      "coverage": { "product_name": "Select Homeowners", "program_type": "quoted_line" },
      "attributes": { "quote_id": "qt_carrier_a_home_001", "line_premium_annual": 6120, "package_premium_annual": 12840 }
    },
    {
      "id": "quote_line_carrier_b_home_001",
      "provider_id": "prov_carrier_b",
      "provider_name": "Carrier B",
      "category": "home",
      "quality_rating": "A",
      "pricing_tier": "economy",
      "commission": 0.12,
      "constraints": { "excluded_states": [], "prohibited_naics": [], "avoided_naics": [] },
      "coverage": { "product_name": "Select Home", "program_type": "quoted_line" },
      "attributes": { "quote_id": "qt_carrier_b_home_001", "line_premium_annual": 5050, "package_premium_annual": 10950 }
    }
  ]
}
```

Results may include an all-Carrier A bundle, an all-Carrier B bundle, and mixed-provider combinations
(e.g. Carrier A home + Carrier C auto): all ranked by composite scorecard.

## Polling

Poll `GET /api/v1/runs/{run_id}` until terminal. Start at 2s delay, backoff 1.5x, cap at 30s.

| Status       | Action                                               |
| ------------ | ---------------------------------------------------- |
| `queued`     | Keep polling                                         |
| `processing` | Keep polling                                         |
| `completed`  | Consume `solutions`                                  |
| `degraded`   | Consume the available output and warnings, then stop |
| `failed`     | Read `error`, stop                                   |

The terminal set is exactly `completed`, `degraded`, and `failed`. A degraded
run is terminal. Never keep polling it.

```python theme={null}
import time, httpx

def wait_for_result(api_key: str, run_id: str) -> dict:
    headers = {"Authorization": f"Bearer {api_key}"}
    delay = 2
    while True:
        data = httpx.get(
            f"https://api.usenexio.com/api/v1/runs/{run_id}",
            headers=headers,
        ).json()
        if data["status"] in ("completed", "degraded", "failed"):
            return data
        time.sleep(delay)
        delay = min(delay * 1.5, 30)
```

## Reading results

Key fields on a completed response:

| Field                                         | Purpose                                                                  |
| --------------------------------------------- | ------------------------------------------------------------------------ |
| `solutions[*].rank`                           | Rank order (1 = best)                                                    |
| `solutions[*].cluster_label`                  | Solution label: `recommended`, `best_value`, `best_coverage`, `simplest` |
| `solutions[*].scorecard.overall_level`        | Raw weighted level. Use `rank`, not this value, for ordering.            |
| `solutions[*].provider_count`                 | 1 = single provider, 2+ = mixed                                          |
| `solutions[*].est_cost_low` / `est_cost_high` | Annual cost range                                                        |
| `solutions[*].offerings[*].provider_name`     | Provider per category                                                    |
| `solutions[*].offerings[*].category`          | Requirement category (domain-specific, e.g. `home`, `auto`)              |

There is no `solutions[*].carrier_name`: use `offerings[*].provider_name`.
There is no `solutions[*].score`: use `scorecard.overall_level`.
Treat `solutions[*].rank` as the ordering contract.

## Failure cases

**`completed` with 0 solutions**: `output.diagnostic` explains why. Common causes:
`coverage_types` / `offerings[*].category` mismatch, missing required categories, or all offerings
filtered out. This is a valid terminal state, not an error.

**`failed`**: read `error` and `error_details`. Terminal.

**`degraded`**: read the output, `warnings` when present, and any documented
engine-specific diagnostics. Terminal.

**Stuck in `processing`**: the `EVALUATE` stage can take time. Use backoff, don't tight-loop.

## Integration checklist

1. `offerings` generated per category per provider, not per package
2. `offerings[*].category` aligns with `input.coverage_types`
3. Polling treats `completed`, `degraded`, and `failed` as terminal
4. Package score read from `scorecard.overall_level`
5. Provider identity read from `offerings[*].provider_name`
6. Both request and terminal response persisted

## Reconciliation doctrine

Persist the `run_id` returned by submission. It is stable for the life of the
run and is the identifier used by polling, webhook payloads, delivery history,
and support. Webhooks are notifications, not the only source of truth. After a
missed, duplicate, delayed, or out-of-order callback, reconcile by polling
`GET /api/v1/runs/{run_id}` with that same ID.

## Example artifacts

* [Canonical request](/assets/example_input_insurance_personal_lines.json): complete `POST /api/v1/engines/default/runs` body with `input` and `offerings` (insurance: personal lines)
* [Canonical response](/assets/example_output_insurance_personal_lines.json): complete `GET /api/v1/runs/{run_id}` response for a completed run (insurance: personal lines)
