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

# Recommend Markets

> Ask which markets fit a risk and get a ranked answer with the evidence, the resolution, and an honest account of what could not be assessed.

## What this returns that browse does not

The rest of the catalog family is browse: you filter markets, products, or appetite cells and page
through what matches. This endpoint answers a question instead. You describe a risk, in free text or
in structured hints, and get back at most five markets in rank order, each with the evidence behind
its position.

It is synchronous and account-free. There is no run to submit, nothing to poll, and no account or
prospect record involved. The resolution, the appetite gate, and the ordering are the same ones the
placement engine applies internally, and this endpoint adds no judgment of its own on top of them:
whether a market will write a class is decided in one place, by one piece of code, for both surfaces.

Candidates are narrowed before the gate by the requested lines, a named excluded state, and the
structured employee and premium bands. Nothing else narrows them. The free-text constraint columns
are not interpreted here, because a value like "Nationwide" in an eligible-states list or "22 or more
locations" in a prohibited-class list cannot be read with enough certainty to remove a market from a
producer's options.

Use browse when you know what you are looking for. Use recommend when you have a risk and need to
know where it can go.

## Asking the question

At least one of `q`, `naics`, `state`, or `lob` must be present. Everything else is optional:

* `q` is free text and is scanned for all three of state, line of business, and business class, so
  `roofing contractor in Colorado needing general liability` alone is a complete question.
* `naics` takes a code at any grain. A code with no entry in the keyword bridge still resolves and is
  echoed back without a keyword rather than being dropped.
* `state` accepts `CO` or `Colorado`.
* `lob` is repeatable and classified through the shared line taxonomy, so everyday phrasing works:
  `lob=commercial property` resolves to the code `property`.
* `employees` and `premium_band` (estimated annual premium in whole US dollars) are used only for
  positive exclusion. A product is dropped when its own stated band excludes the value. A product
  that states no band is never dropped, because absent data is not a statement of ineligibility.

Every request echoes what the platform understood on `resolved`, and names what it could not resolve
on `missing_context` (`state`, `line_of_business`, `business_class`). A partial question still gets
an answer built from the parts that did resolve; the caller decides whether the gap is worth a
clarifying question.

## What verified each match

`match_status` says what actually stands behind a recommendation, and `confidence` is a
deterministic restatement of it rather than a separate score.

| `match_status`           | What it means                                                                                                                | `confidence` |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ------------ |
| `class_appetite_matched` | A current appetite cell positively matched the resolved business class.                                                      | `high`       |
| `line_level_only`        | The class resolved, but this market's appetite is not assessed for it. Served line data.                                     | `moderate`   |
| `class_unresolved`       | No business class resolved. The match rests on the requested line only.                                                      | `low`        |
| `no_verified_match`      | Neither a class appetite cell nor a requested line verified the fit; the market exists in the corpus for the resolved scope. | `low`        |

Exactly one applies to each market. A `line_level_only` result is a real answer, not a failure: it
says this market serves the line and nobody has recorded an appetite position on the class.

## Reading the appetite block

`appetite` is present only when a current guide cell positively matched, and it is attributed to a
document. Two things follow from that, and both matter:

**`strength` is the guide cell's own eligibility word.** It is not the product row's served appetite
strength, and the two can disagree. When a product row says `restricted` and the guide cell says
`acceptable`, this block reports `acceptable`, because it is quoting the guide, not summarizing the
market. If you need the served value, read the product row through the browse endpoints.

**`line_code` names which line the evidence covers.** A market can come back with several products
while the cell speaks to one line. The line named here is always present in `products`, even when the
per-market product cap would otherwise have dropped it, so the claim is always checkable against a
row you were given. Do not read the appetite block as coverage of every product listed.

A market whose guide positively declined the resolved class and line is never recommended at all.
Only a known decline word excludes: an unknown, empty, or newly added eligibility leaves the market
in the answer as unassessed, because absence of a statement is not a decline.

## Why a market ranks where it does

`rank_reasons` narrate only signals the ranking comparator actually read: appetite strength, then AM
Best rating, then pricing tier. It is a short array and it can be empty when the market's best
offering carried none of them.

Appetite appears as a rank reason only when the line the cell matched is also the line that was
ranked, and the value the comparator read agrees with the cell. When it does not, the appetite is
still returned in the `appetite` block. It explains the match; it did not drive the position.

Product count is not a reason, and never will be: the comparator does not read it. The `products`
array itself shows the served lines in scope.

**Commission never affects ordering.** `commission_new` and `commission_renewal` are display-only
disclosed values. Nothing about the rank of a market is a function of what it pays.

## What is never defaulted

* `am_best_rating` is `null` when the corpus holds no rating. Null means not known. It is not
  "unrated", and it is not a low rating.
* `commission_new` and `commission_renewal` are omitted when the rate was never disclosed. An
  omitted rate is not a zero percent commission. Coalescing it to `0` will misrank a market in your
  own display and misinform a producer.
* `stale` is tri-state: `true` (assessed, out of date), `false` (assessed, current), and absent
  (never assessed). Absent and `false` are different facts. Treating absent as `false` presents an
  unassessed schedule as verified-current.
* `source_blob_ref` and `extraction_run_id` are omitted when lineage was not recorded. They are never
  fabricated.

## When the answer is partial or empty

Two fields carry the limits of the answer, and they say different things.

`no_coverage` carries the reason nothing could be answered: the query resolved to no class, line, or
state; the organization has no active catalog connection; or every requested line is outside this
catalog's scope. It is also set when the class resolved but the corpus holds no appetite data for it,
and in that case markets are still listed by line and state and the message says so. Surface the
message; do not branch on its presence alone.

```json theme={null}
{
  "query": { "q": "quantum harmonics for left-handed dentists" },
  "resolved": { "state": null, "naics": [], "lob_codes": [] },
  "missing_context": ["state", "line_of_business", "business_class"],
  "coverage": {
    "markets_total": 0,
    "markets_with_appetite_data": 0,
    "appetite_assessable": false
  },
  "no_coverage": "The query resolved to no known class, line of business, or state; nothing in the taxonomy matches it.",
  "recommendations": []
}
```

`lines_not_assessed` is a scope caveat that rides alongside results. This catalog serves commercial
lines and surety, while the taxonomy also resolves personal lines, so a mixed request is answered for
the lines in scope and told which ones were left out:

```json theme={null}
{
  "lines_not_assessed": ["Homeowners"],
  "recommendations": [{ "market_entity_key": "acuity", "...": "..." }]
}
```

When every requested line is out of scope there is nothing to answer, so `no_coverage` is set too and
`recommendations` is empty.

## Connections

Like the rest of the catalog family, the read is scoped to one data connection. Organizations with a
single active connection can omit `connection_id`; organizations with more than one must pass it, and
an ambiguous request returns `catalog_connection_ambiguous` with a 400. A `connection_id` that is not
an active connection for the organization returns `404 not_found` rather than an empty answer, so a
pinned client that loses its connection fails loud. An organization with no active connection gets a
200 whose `no_coverage` says exactly that, rather than an answer that reads as "no market wants this".

## Example

```bash theme={null}
curl -s -G "https://api.usenexio.com/api/v1/catalog/recommend" \
  --data-urlencode "q=roofing contractor in Colorado" \
  --data-urlencode "naics=238160" \
  --data-urlencode "lob=general liability" \
  --data-urlencode "lob=commercial property" \
  --data-urlencode "employees=45" \
  -H "Authorization: Bearer $NEXIO_API_KEY"
```

```json theme={null}
{
  "query": {
    "q": "roofing contractor in Colorado",
    "naics": "238160",
    "lob": ["general liability", "commercial property"],
    "employees": 45
  },
  "resolved": {
    "state": "CO",
    "naics": [{ "code": "238160", "keyword": "roofing" }],
    "lob_codes": [
      { "code": "general_liability", "label": "General Liability" },
      { "code": "property", "label": "Commercial Property" }
    ]
  },
  "missing_context": [],
  "coverage": {
    "markets_total": 148,
    "markets_matching_class": 12,
    "markets_with_appetite_data": 96,
    "appetite_assessable": true
  },
  "recommendations": [
    {
      "market_entity_key": "acuity",
      "name": "Acuity",
      "am_best_rating": "A+",
      "rank_reasons": [
        "Appetite guide lists \"Roofing Contractors\" as acceptable",
        "AM Best rating A+",
        "Pricing tier: standard"
      ],
      "appetite": {
        "strength": "acceptable",
        "business_class": "Roofing Contractors",
        "line_code": "general_liability",
        "source_blob_ref": "acuity/appetite_guide/9f3c1a",
        "extraction_run_id": "6b1f0d54-9c22-4a31-84f0-1d5b2f0a7e10"
      },
      "products": [
        {
          "line_code": "general_liability",
          "label": "General Liability",
          "product_name": "Acuity Contractors GL",
          "commission_new": 0.15,
          "commission_renewal": 0.125,
          "source_blob_ref": "acuity/commission_schedule/4b81de",
          "extraction_run_id": "6b1f0d54-9c22-4a31-84f0-1d5b2f0a7e10",
          "effective_date": "2026-01-01",
          "expiration_date": "2026-12-31",
          "stale": false
        },
        {
          "line_code": "property",
          "label": "Commercial Property",
          "product_name": "Acuity Commercial Property",
          "source_blob_ref": "acuity/commission_schedule/4b81de",
          "extraction_run_id": "6b1f0d54-9c22-4a31-84f0-1d5b2f0a7e10"
        }
      ],
      "confidence": "high",
      "match_status": "class_appetite_matched"
    },
    {
      "market_entity_key": "western-national",
      "name": "Western National",
      "am_best_rating": null,
      "rank_reasons": ["Pricing tier: value"],
      "products": [
        {
          "line_code": "general_liability",
          "label": "General Liability",
          "product_name": "Western National Contractors Package",
          "commission_renewal": 0.1,
          "source_blob_ref": "western-national/commission_schedule/07c2aa",
          "extraction_run_id": "0e5a7c31-4b18-4a02-9c77-8f6d2b41c905",
          "effective_date": "2024-07-01",
          "stale": true
        }
      ],
      "confidence": "moderate",
      "match_status": "line_level_only"
    }
  ]
}
```

The second market is the one worth reading twice. It has no AM Best rating on file, its general
liability row discloses a renewal rate but no new-business rate, and its schedule has been assessed
and found out of date. Three separate absences, three different meanings, and not one of them is a
zero. Rendering `am_best_rating` as `0`, or `commission_new` as `0%`, would put a number in front of
a producer that no document ever stated.

Its `rank_reasons` also carry only one entry, because the comparator had only one signal to read on
it. That is the contract working: the array is a record of what was read, not a summary written after
the fact.

```python theme={null}
import os
import requests

response = requests.get(
    "https://api.usenexio.com/api/v1/catalog/recommend",
    headers={"Authorization": f"Bearer {os.environ['NEXIO_API_KEY']}"},
    params={
        "q": "roofing contractor in Colorado",
        "naics": "238160",
        "lob": ["general liability", "commercial property"],
        "employees": 45,
    },
    timeout=30,
)
response.raise_for_status()
answer = response.json()

if answer.get("no_coverage"):
    print(answer["no_coverage"])
for line in answer.get("lines_not_assessed", []):
    print(f"not assessed by this catalog: {line}")

for rec in answer["recommendations"]:
    # null means the corpus holds no rating. It is not "unrated" and not a 0.
    rating = "no rating on file" if rec["am_best_rating"] is None else rec["am_best_rating"]
    print(f'{rec["name"]} [{rec["match_status"]}, {rec["confidence"]}] {rating}')
    for reason in rec["rank_reasons"]:
        print(f"  ranked on: {reason}")

    appetite = rec.get("appetite")
    if appetite:
        # The guide's own word, covering the one line it names.
        line = appetite.get("line_code", "an unnamed line")
        print(f'  guide: {appetite["business_class"]} is {appetite["strength"]} for {line}')

    for product in rec["products"]:
        # An absent commission is undisclosed. Never fall back to 0 here.
        if "commission_new" in product:
            new_rate = f'{product["commission_new"] * 100:.2f}%'
        else:
            new_rate = "not disclosed"
        # stale is tri-state: absent is "never assessed", not "current".
        if "stale" not in product:
            currency = "currency never assessed"
        else:
            currency = "out of date" if product["stale"] else "current"
        print(f'  {product["line_code"]}: new {new_rate} ({currency})')
```

```typescript theme={null}
const params = new URLSearchParams({
  q: 'roofing contractor in Colorado',
  naics: '238160',
  employees: '45',
})
params.append('lob', 'general liability')
params.append('lob', 'commercial property')

const response = await fetch(
  `https://api.usenexio.com/api/v1/catalog/recommend?${params}`,
  { headers: { Authorization: `Bearer ${process.env.NEXIO_API_KEY}` } },
)
if (!response.ok) throw new Error(`recommend failed: ${response.status}`)
const answer = await response.json()

if (answer.no_coverage) console.log(answer.no_coverage)
for (const line of answer.lines_not_assessed ?? []) {
  console.log(`not assessed by this catalog: ${line}`)
}

for (const rec of answer.recommendations) {
  // null means no rating on file. `?? 'A'` or `?? 0` would invent one.
  const rating = rec.am_best_rating === null ? 'no rating on file' : rec.am_best_rating
  console.log(`${rec.name} [${rec.match_status}, ${rec.confidence}] ${rating}`)
  for (const reason of rec.rank_reasons) console.log(`  ranked on: ${reason}`)

  if (rec.appetite) {
    // The guide's own word, covering the one line it names.
    const line = rec.appetite.line_code ?? 'an unnamed line'
    console.log(`  guide: ${rec.appetite.business_class} is ${rec.appetite.strength} for ${line}`)
  }

  for (const product of rec.products) {
    // commission_new is ABSENT when undisclosed. `?? 0` would invent a 0% rate.
    const newRate =
      product.commission_new === undefined
        ? 'not disclosed'
        : `${(product.commission_new * 100).toFixed(2)}%`
    // stale is tri-state: undefined is "never assessed", not "current".
    const currency =
      product.stale === undefined
        ? 'currency never assessed'
        : product.stale
          ? 'out of date'
          : 'current'
    console.log(`  ${product.line_code}: new ${newRate} (${currency})`)
  }
}
```


## OpenAPI

````yaml GET /api/v1/catalog/recommend
openapi: 3.1.0
info:
  title: Nexio API
  version: '1.0'
  description: |
    Nexio agentic infrastructure API. Configure engines for placement,
    entity analysis, and other patterns. Submit runs, poll for results,
    or subscribe to signed webhook callbacks.
  contact:
    email: support@usenexio.com
    url: https://docs.usenexio.com
servers:
  - url: https://api.usenexio.com
    description: Production
security:
  - BearerAuth: []
tags:
  - name: EngineManagement
    description: Create, configure, and manage inference engines.
  - name: Engines
    description: Engine-scoped endpoints for submitting runs and retrieving results.
  - name: Runs
    description: Retrieve and reconcile submitted runs.
  - name: Catalog
    description: >-
      Read current markets and products extracted for the authenticated
      organization.
  - name: Environments
    description: Isolated tenancy scopes under your org (one live plus non-live sandboxes).
  - name: Webhooks
    description: Manage webhook endpoints for push-based delivery of terminal run events.
  - name: Converse
    description: The raw stateless conversational model turn (caller-owned state).
  - name: Conversations
    description: >-
      Conversation instances (orchestrators over engines) and their managed
      conversations, turns, annotations, evals, and exports.
paths:
  /api/v1/catalog/recommend:
    get:
      tags:
        - Catalog
      summary: Recommend Markets
      description: |
        Answers one placement question synchronously against the authenticated
        organization's served catalog: given free text plus any structured
        hints, which markets fit, ranked, with the evidence behind each. No
        account and no run are involved. Scoped credentials require
        `catalog:read`.

        At least one of `q`, `naics`, `state`, or `lob` is required. The inputs
        resolve through the same taxonomy the rest of the catalog family uses,
        and the resolution is echoed on `resolved` so a caller can see exactly
        what the platform understood. `missing_context` names the critical
        inputs that did not resolve (`state`, `line_of_business`,
        `business_class`); the answer is still returned with whatever did.

        Ordering reuses the placement engine's own comparator: appetite
        strength, then AM Best rating, then pricing tier, then completeness
        tie-breaks. Commission is display only and can never affect ordering.
        At most 5 markets are returned, each carrying up to 15 products, plus
        the line the appetite evidence rests on when the cap would otherwise
        have hidden it.

        Honesty rules a consumer must not smooth over:

        - `recommendations[].appetite.strength` is the appetite guide cell's OWN
          eligibility word. It is not the product row's served appetite
          strength, and the two can disagree. This block is attributed to a
          document, so every value in it is that document's claim.
        - `recommendations[].appetite.line_code` names which line the evidence
          covers, and that line is always present in `products`. It is not a
          claim about the market's other listed lines.
        - `rank_reasons` narrate only signals the ranking comparator actually
          read. Appetite appears there only when the matched line is also the
          ranked line AND the value the comparator read agrees with the cell.
        - A market whose appetite guide positively declined the resolved class
          and line is never recommended. Only a known decline word excludes; an
          unknown or empty eligibility does not, because absence of a statement
          is not a decline.
        - `products[].stale` is tri-state: `true` (assessed, out of date),
          `false` (assessed, current), absent (never assessed). Absent and
          `false` are different facts.
        - An absent AM Best rating is `null`. An undisclosed commission is
          omitted. Nothing is defaulted, and neither `null` nor absent ever
          means zero.
        - `no_coverage` carries the reason nothing could be answered (the query
          resolved to no class, line, or state; the org has no active catalog
          connection; every requested line is outside this catalog's scope). It
          is also set when the class resolved but the corpus holds no appetite
          data for it, and in that one case markets are still listed by line and
          state and the message says so. Read the message, not just its
          presence.
        - `lines_not_assessed` is a partial-scope caveat that rides alongside
          results: this catalog serves commercial lines and surety, while the
          taxonomy also resolves personal lines. It names the requested lines
          the answer does not cover.
      operationId: recommendCatalogMarkets
      parameters:
        - $ref: '#/components/parameters/CatalogRecommendQuery'
        - $ref: '#/components/parameters/CatalogRecommendNAICS'
        - $ref: '#/components/parameters/CatalogRecommendState'
        - $ref: '#/components/parameters/CatalogRecommendLob'
        - $ref: '#/components/parameters/CatalogRecommendEmployees'
        - $ref: '#/components/parameters/CatalogRecommendPremiumBand'
        - $ref: '#/components/parameters/CatalogConnectionID'
      responses:
        '200':
          description: |
            The resolution, its corpus coverage, and up to 5 ranked markets.
            A question the corpus cannot answer is still a 200: the honest
            answer is the echo plus `missing_context` plus `no_coverage`, with
            an empty `recommendations` array.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CatalogRecommendResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
components:
  parameters:
    CatalogRecommendQuery:
      name: q
      in: query
      description: >-
        Free-text description of the risk, for example `roofing contractor in
        Colorado needing general liability`. State, lines of business, and
        business class are all scanned out of it. At least one of `q`, `naics`,
        `state`, or `lob` must be present.
      schema:
        type: string
        maxLength: 500
    CatalogRecommendNAICS:
      name: naics
      in: query
      description: >-
        NAICS business-class code, at any grain (`23`, `2381`, `238160`). A code
        with no entry in the keyword bridge still resolves and is echoed back
        with no keyword, rather than being dropped.
      schema:
        type: string
        maxLength: 10
    CatalogRecommendState:
      name: state
      in: query
      description: >-
        US state as a two-letter code or a full name (`CO` and `Colorado` both
        resolve). Unresolvable text leaves `resolved.state` null and puts
        `state` in `missing_context`; it is not an error.
      schema:
        type: string
        maxLength: 50
    CatalogRecommendLob:
      name: lob
      in: query
      description: >-
        Line of business, repeatable
        (`?lob=general+liability&lob=workers+comp`). Each value is classified
        through the shared line taxonomy, so both canonical codes and everyday
        phrasing work. At most 10 values, each at most 200 characters.
      explode: true
      style: form
      schema:
        type: array
        maxItems: 10
        items:
          type: string
          maxLength: 200
    CatalogRecommendEmployees:
      name: employees
      in: query
      description: >-
        Applicant employee count. Used only for positive exclusion: a product is
        dropped when its own stated employee band excludes the number. Absent
        band data never excludes.
      schema:
        type: integer
        minimum: 1
    CatalogRecommendPremiumBand:
      name: premium_band
      in: query
      description: >-
        Estimated annual premium in whole US dollars. Used only for positive
        exclusion against a product's stated premium band, the same way
        `employees` is used.
      schema:
        type: integer
        minimum: 1
    CatalogConnectionID:
      name: connection_id
      in: query
      description: >
        Pin the browse to one active data connection. Optional when the org has
        a

        single active connection; required when it has more than one (an
        ambiguous

        request returns `catalog_connection_ambiguous`).
      schema:
        type: string
        format: uuid
  schemas:
    CatalogRecommendResponse:
      type: object
      additionalProperties: false
      required:
        - query
        - resolved
        - missing_context
        - coverage
        - recommendations
      properties:
        query:
          $ref: '#/components/schemas/CatalogRecommendQueryEcho'
        resolved:
          $ref: '#/components/schemas/CatalogRecommendResolved'
        missing_context:
          type: array
          description: >-
            Critical inputs the resolution could not supply, in a fixed order.
            The consumer decides whether to ask a clarifying question; the
            platform still answers with what it has.
          items:
            type: string
            enum:
              - state
              - line_of_business
              - business_class
        coverage:
          $ref: '#/components/schemas/CatalogRecommendCoverage'
        no_coverage:
          type: string
          description: >-
            Present when the corpus could answer nothing (the query resolved to
            no class, line, or state; the org has no active catalog connection;
            every requested line is outside this catalog's scope), and also when
            the class resolved but no served appetite data exists for it. In
            that last case markets are still listed by line and state, and the
            message says so. Read the message rather than branching on presence
            alone.
        lines_not_assessed:
          type: array
          description: >-
            Requested lines this catalog could not assess, named in the labels
            the resolution produced. This catalog serves commercial lines and
            surety while the taxonomy also resolves personal lines, so a mixed
            request is answered for the lines in scope and this array says which
            ones were left out. It is a partial-scope caveat that rides
            alongside results, not a terminal state. When EVERY requested line
            is out of scope, `no_coverage` is set as well and `recommendations`
            is empty.
          items:
            type: string
        recommendations:
          type: array
          maxItems: 5
          description: >-
            Ranked markets, best first, at most 5. Always present; an empty
            array is the honest answer for a question with no matching market.
          items:
            $ref: '#/components/schemas/CatalogRecommendation'
    CatalogRecommendQueryEcho:
      type: object
      additionalProperties: false
      description: >-
        The request the answer was computed for, echoed back. Fields the caller
        did not send are omitted.
      properties:
        q:
          type: string
        naics:
          type: string
        state:
          type: string
        lob:
          type: array
          items:
            type: string
        employees:
          type: integer
        premium_band:
          type: integer
          description: Estimated annual premium in whole US dollars, as sent.
    CatalogRecommendResolved:
      type: object
      additionalProperties: false
      required:
        - state
        - naics
        - lob_codes
      description: >-
        What the platform understood the query to mean. Same shape the taxonomy
        resolver serves.
      properties:
        state:
          type: string
          nullable: true
          description: >-
            Two-letter state code, or null when no state resolved. Null pairs
            with `state` in `missing_context`.
        naics:
          type: array
          description: Resolved business classes. Empty when none resolved.
          items:
            $ref: '#/components/schemas/CatalogRecommendResolvedNAICS'
        lob_codes:
          type: array
          description: Resolved lines of business. Empty when none resolved.
          items:
            $ref: '#/components/schemas/CatalogRecommendResolvedLob'
    CatalogRecommendCoverage:
      type: object
      additionalProperties: false
      required:
        - markets_total
        - markets_with_appetite_data
        - appetite_assessable
      description: >-
        What the served corpus can actually answer for this resolution,
        including the honest zeroes.
      properties:
        markets_total:
          type: integer
          description: Current markets in the connection, ignoring the query.
        markets_matching_class:
          type: integer
          description: >-
            Markets holding an appetite cell for the resolved business class.
            Omitted entirely when no business class resolved, because the count
            would be a statement about a question nobody asked.
        markets_with_appetite_data:
          type: integer
          description: Markets holding any appetite data at all.
        appetite_assessable:
          type: boolean
          description: >-
            True when at least one market has appetite data for the resolved
            class. False means class-level fit cannot be assessed from appetite;
            markets can still be listed by line and state.
    CatalogRecommendation:
      type: object
      additionalProperties: false
      required:
        - market_entity_key
        - name
        - am_best_rating
        - rank_reasons
        - products
        - confidence
        - match_status
      properties:
        market_entity_key:
          type: string
          description: >-
            Canonical market identity, the same key `GET
            /api/v1/catalog/markets/{entity_key}` accepts.
        name:
          type: string
          description: Market name as the served corpus carries it.
        am_best_rating:
          type: string
          nullable: true
          description: >-
            AM Best rating, or null when the corpus holds none. Null means not
            known, never unrated and never a default.
        rank_reasons:
          type: array
          description: >-
            Why this market sits where it sits, in comparator order, naming ONLY
            signals the ranking comparator actually read (appetite strength, AM
            Best rating, pricing tier). Appetite appears here only when the
            matched line is also the ranked line and the value the comparator
            read agrees with the cell; otherwise the appetite is still returned
            as evidence in `appetite`, just not claimed as the reason for the
            position; a market carrying a served appetite strength with no
            matching guide cell reports that instead. Product count is
            deliberately absent: the comparator never reads it. The array can be
            empty, which means none of these signals distinguished the market:
            the comparator then falls through to record completeness and a
            stable identifier, and naming either would assert a reason the data
            does not support.
          items:
            type: string
        appetite:
          $ref: '#/components/schemas/CatalogRecommendAppetite'
        products:
          type: array
          description: >-
            The market's served lines in scope, in rank order, capped at 15. The
            line named by `appetite.line_code` is always included even if the
            cap would otherwise have dropped it.
          items:
            $ref: '#/components/schemas/CatalogRecommendProduct'
        confidence:
          type: string
          enum:
            - high
            - moderate
            - low
          description: >-
            Derived deterministically from `match_status`:
            `class_appetite_matched` is `high`, `line_level_only` is `moderate`,
            everything else is `low`. It is a restatement of the evidence level,
            not a separate score.
        match_status:
          type: string
          enum:
            - class_appetite_matched
            - line_level_only
            - class_unresolved
            - no_verified_match
          description: >-
            What actually verified this match. `class_appetite_matched`: a
            current appetite cell positively matched the resolved business
            class. `line_level_only`: the class resolved but this market's
            appetite is not assessed for it, so the match rests on served line
            data. `class_unresolved`: no business class resolved, so the match
            rests on the requested line only. `no_verified_match`: neither a
            class appetite cell nor a requested line verified the fit; the
            market merely exists in the corpus for the resolved scope. Exactly
            one applies.
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: |
            Stable snake_case error identifier. Safe to match programmatically.

            Known codes: `invalid_request`, `invalid_input`, `unauthorized`,
            `auth_unavailable`, `rate_limited`, `missing_run_id`,
            `invalid_run_id`, `run_not_found`, `invalid_offerings`,
            `missing_input`, `queue_unreachable`,
            `engine_not_found`, `engine_slug_conflict`, `engine_archived`,
            `instance_not_found`, `instance_slug_conflict`, `instance_archived`,
            `instance_follows_canonical`, `instance_not_published`,
            `invalid_engine_type`, `validation_error`,
            `webhook_not_found`, `webhook_limit_exceeded`, `invalid_url`,
            `invalid_events`, `invalid_description`, `invalid_auth_token`,
            `missing_endpoint_id`, `invalid_endpoint_id`, `missing_delivery_id`,
            `invalid_delivery_id`, `delivery_not_found`,
            `delivery_not_resendable`, `insufficient_capability`,
            `engine_version_required`, `engine_version_exact_required`,
            `engine_version_not_found`, `engine_version_invalid_format`,
            `engine_version_draft_requires_sandbox_key`,
            `engine_version_none_released`, `test_scenario_sandbox_only`,
            `test_scenario_forbidden`, `test_scenario_exact_version_required`,
            `test_scenario_version_not_supported`, `invalid_test_scenario`,
            `request_bound_exceeded`, and `run_cap_exceeded`.
        message:
          type: string
          description: Human-readable error message. May change between versions.
        details:
          description: |
            Optional request-specific details. Request-bound failures use the
            `RequestBoundDetails` object. Validation failures may use an array
            of field issues or another documented object.
    CatalogRecommendResolvedNAICS:
      type: object
      additionalProperties: false
      required:
        - code
      properties:
        code:
          type: string
          description: The NAICS code the query evidenced.
        keyword:
          type: string
          description: >-
            The bridge keyword that produced the code, so the resolution is
            auditable. Omitted when the code was supplied directly and has no
            bridge entry.
    CatalogRecommendResolvedLob:
      type: object
      additionalProperties: false
      required:
        - code
        - label
      properties:
        code:
          type: string
          description: Canonical line-of-business code, for example `general_liability`.
        label:
          type: string
          description: Display label for the code, for example `General Liability`.
    CatalogRecommendAppetite:
      type: object
      additionalProperties: false
      required:
        - strength
        - business_class
      description: >-
        The appetite guide cell behind a recommendation. Present only when a
        current cell positively matched the resolved class (and the line, when
        the cell names one). This block is attributed to a document, so every
        value in it is that document's own claim.
      properties:
        strength:
          type: string
          description: >-
            The guide cell's OWN eligibility word, lowercased. It is NOT the
            product row's served appetite strength, and the two can disagree: a
            product row reading `restricted` beside a guide cell reading
            `acceptable` would misattribute a claim to a document that never
            made it. Today the values that can reach a recommendation are
            `acceptable` and `will_consider`; treat the field as an open string
            rather than a closed enum.
        business_class:
          type: string
          description: The business-class label as the guide printed it.
        line_code:
          type: string
          description: >-
            Which of the market's lines this evidence covers, when the guide
            cell named one. The same line always appears in `products`, so the
            claim is checkable, and a market can be recommended with several
            products while the evidence covers only this line.

            OMITTED means the appetite cell was line-agnostic: the guide stated
            appetite for the class across the market rather than for one line.
            That is broader evidence, not missing metadata, and it is left unset
            rather than filled from the matched offering because naming a line
            the document did not name would attribute a narrower claim than the
            guide made.
        source_blob_ref:
          type: string
          description: >-
            Source document the cell was extracted from. Omitted when
            unrecorded.
        extraction_run_id:
          type: string
          format: uuid
          description: Extraction run that produced the cell. Omitted when unrecorded.
    CatalogRecommendProduct:
      type: object
      additionalProperties: false
      required:
        - line_code
      description: >-
        One served product line behind a recommendation. Commission values are
        display-only disclosed scalars and never influence ordering.
      properties:
        line_code:
          type: string
          description: Canonical line code, for example `general_liability`.
        label:
          type: string
          description: >-
            Display label for the line. Omitted when the code has no taxonomy
            label.
        product_name:
          type: string
          description: >-
            Product name as the source document printed it. Omitted when
            unstated.
        commission_new:
          type: number
          description: >-
            New-business commission as a fraction (0.15 means 15 percent).
            OMITTED when the served read did not mark the rate known. An omitted
            rate means not disclosed and is never a zero commission; do not
            coalesce it to 0.
        commission_renewal:
          type: number
          description: >-
            Renewal commission as a fraction, omitted on the same terms as
            `commission_new`.
        source_blob_ref:
          type: string
          description: Source document behind this product row. Omitted when unrecorded.
        extraction_run_id:
          type: string
          format: uuid
          description: >-
            Extraction run that produced this product row. Omitted when
            unrecorded.
        effective_date:
          type: string
          description: Start of the schedule's stated validity window, when assessed.
        expiration_date:
          type: string
          description: End of the schedule's stated validity window, when stated.
        stale:
          type: boolean
          description: >-
            Data-currency verdict, tri-state on the wire. `true` means assessed
            and out of date, `false` means assessed and current, and the field
            is ABSENT when currency was never assessed. Absent and `false` are
            different facts: collapsing them presents an unassessed schedule as
            verified-current. Advisory only, never a ranking input.
  responses:
    BadRequest:
      description: Invalid request.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidRequest:
              summary: Malformed JSON
              value:
                code: invalid_request
                message: Request body is not valid JSON
            invalidInput:
              summary: Input validation failed
              value:
                code: invalid_input
                message: Input failed validation
                details:
                  - field: input.address.state
                    message: address.state is required for placement runs.
            invalidOfferings:
              summary: Placement run missing offerings
              value:
                code: invalid_offerings
                message: Inline offerings failed validation
                details:
                  - field: offerings
                    message: >-
                      Offerings are required for placement runs. Pass provider
                      offerings in the top-level offerings array.
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: unauthorized
            message: Missing or invalid API key
    Forbidden:
      description: Scoped key does not have the capability required by this operation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: insufficient_capability
            message: API key does not have permission for this action
    NotFound:
      description: The requested resource does not exist in the authenticated org.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: not_found
            message: Market not found
    RateLimited:
      description: Rate limit exceeded. Retry after the window resets.
      headers:
        Retry-After:
          description: Seconds until the rate limit window resets.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: rate_limited
            message: Rate limit exceeded
    AuthUnavailable:
      description: API key authentication is temporarily unavailable.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: auth_unavailable
            message: API key authentication is temporarily unavailable
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        Send the credential as `Authorization: Bearer <key>`.

        Scoped partner credentials use the exclusive `nxsk_v1_...` namespace.
        Each scoped key is bound at issuance to one organization, one canonical
        named environment, an explicit engine set, and a least-privilege
        capability set. A malformed, unknown, rotated, or revoked `nxsk_` key
        fails closed and is never retried as a legacy key.

        Capabilities used by this API are `runs:write`, `runs:read`,
        `engines:read`, `catalog:read`, `webhooks:manage`,
        `runs:defensibility:read`, `runs:test`, `conversations:use`, and
        `conversations:export`. Operation descriptions name the required
        capability.
        Grandfathered `nx_live_...` and `nx_test_...` keys retain their existing
        broad access during the compatibility window.

````