# ─────────────────────────────────────────────────────────────────────────
# DO NOT EDIT — this file is synced from /public/openapi.yaml at build time.
# Source of truth: <repo root>/public/openapi.yaml
# Sync script:    apps/docs/scripts/sync-openapi.ts
# Any edits here will be overwritten on the next `npm run dev` or `npm run build`.
# ─────────────────────────────────────────────────────────────────────────
openapi: 3.1.0
info:
  title: Skadi Specialty Insurance Co. Partner API
  summary: E&S excess casualty rating, quoting, binding, and policy lifecycle.
  description: |
    Partner-facing surface for Skadi Specialty Insurance Co.'s excess casualty platform.
    Covers rating, appetite scoring, hazard lookup, submissions, the full
    quote-then-bind transaction lifecycle (endorsements, cancellations,
    extensions, audits, renewals, rewrites, OOS rebases), and read access
    to policies / claims / documents / reports.

    The narrative version of this contract lives on the
    [developer portal](https://developers.skadispecialty.com/getting-started).
    Treat the YAML here as the machine-readable mirror.

    **Two distribution channels, two key shapes.** Skadi distributes through
    appointed wholesalers via two channels. The API is shaped to match.

    - **Binding-authority producers** get the full surface — rate, bind,
      issue, endorse, cancel, reinstate, renew. Scoped to their letter of
      authority. Out-of-authority risks are referred to Skadi's binding
      desk via the same flow (see `/binding-authority` on the marketing
      site). Default preset: `binding-authority`.
    - **Brokerage producers** submit risks that Skadi underwriters work
      directly. They do not bind in Skadi's name. Their API surface is
      reads (status, documents, policies, reports) plus indicative
      rating utilities (`rate`, `appetite`, `hazard`) for pre-screening.
      Submission intake itself is currently email-only — see
      `/brokerage` on the marketing site. Default preset:
      `brokerage-producer`.

    Preset selection happens at key-issuance time; admins pick the preset
    that matches the partner's appointment type. See `x-skadi-scopes.presets`
    below.

    **Authentication:** every endpoint except `/health-check` and `/oauth-token`
    requires either HMAC (Mode A — preferred) or OAuth Bearer (Mode B). See
    `securitySchemes` below for the wire format. Variance binds (overriding
    the server-rated premium) require an additional internal-only scope and
    are not granted to partner keys.

    **Idempotency:** all create + bind endpoints accept an `Idempotency-Key`
    header. Replays return the original response body and status; conflicting
    bodies under the same key produce `409 idempotency-conflict`.

    **Errors:** RFC 7807 Problem Details (`application/problem+json`) with a
    fixed catalog of `type` values. See the `Problem` schema and the
    full list under `components.schemas.ProblemType`.
  version: '2026-04-25'
  contact:
    name: Skadi Partner Engineering
    url: https://skadispecialty.com/contact
    email: partner-eng@skadispecialty.com
  license:
    name: Proprietary — Skadi Specialty Insurance Co.
    url: https://skadispecialty.com/legal/terms
servers:
  - url: https://zsznsjvcluslttkxjhng.supabase.co/functions/v1
    description: Production
  - url: https://tyimzxmwppgiycvcdxvl.supabase.co/functions/v1
    description: Sandbox

tags:
  - name: Auth
    description: OAuth 2.0 client_credentials token exchange.
  - name: Rating
    description: Rating engine — premium computation without persistence.
  - name: Underwriting
    description: Appetite + hazard lookup utilities.
  - name: Submissions
    description: Submit a new account for rate-then-bind.
  - name: Transactions
    description: Quote-then-bind lifecycle (endorsement, cancellation, extension, audit, renewal, rewrite, OOS rebase).
  - name: Reads
    description: Tier 1 + Tier 2 read endpoints over the policy book.
  - name: Reports
    description: Aggregated reporting views.
  - name: System
    description: Operational + introspection endpoints.
  - name: Documents
    description: |
      Rendered-document endpoints. Served from the Vercel app host
      (`https://app.skadispecialty.com`), not the Edge Function origin —
      each operation in this tag declares its own `servers:` block. One
      host serves both environments; the API key prefix (`odn_live_*` /
      `odn_sb_*`) selects live vs sandbox. Same HMAC auth scheme as the
      Edge Function endpoints.

# ------------------------------------------------------------------------
# Security
# ------------------------------------------------------------------------
security:
  - hmacKey: []
  - oauthBearer: []

# ------------------------------------------------------------------------
# Scope catalog (Skadi extension)
#
# Single source of truth for the scopes the partner key request flow
# offers, together with the per-operation `x-skadi-scope` keys below.
# ------------------------------------------------------------------------
x-skadi-scopes:
  groups:
    - id: compute
      label: Compute
      description: Stateless utilities. No data is persisted.
    - id: writes
      label: Writes
      description: Persist new submissions and policy-lifecycle transactions.
    - id: reads
      label: Reads
      description: Tier 1 and Tier 2 read access, scoped to your agency group.
  scopes:
    rate:
      label: Rate quotes
      group: compute
      description: Compute premium for a tower without persisting.
    appetite:
      label: Appetite check
      group: compute
      description: Score an opportunity against the appetite matrix.
    hazard:
      label: Hazard lookup
      group: compute
      description: Resolve a NAICS code to its hazard grade.
    write:submissions:
      label: Create submissions
      group: writes
      description: Persist a submission and a bound quote.
    write:transactions:
      label: Process transactions
      group: writes
      description: Quote-then-bind for endorsements, cancellations, extensions, audits, renewals, rewrites, and OOS rebases. Plus deterministic state-flips (reinstatement, non-renewal).
    write:documents:
      label: Generate documents
      group: writes
      description: Trigger PDF/ACORD form rendering for a quote or policy. Document bytes are returned over the partner PDF endpoint; metadata is then readable via read:documents.
    read:policies:
      label: Read policies
      group: reads
      description: Current policy state and transaction history.
    read:submissions:
      label: Read submissions
      group: reads
      description: Intake records for your agency group.
    read:quotes:
      label: Read quotes
      group: reads
      description: Quote versions and pricing variance.
    read:claims:
      label: Read claims
      group: reads
      description: Post-bind claims, exposures, and totals.
    read:documents:
      label: Read documents (metadata)
      group: reads
      description: Metadata-only listing of forms and documents. No PDF bytes.
    read:reports:
      label: Read reports
      group: reads
      description: Tenant-scoped aggregates (scorecard, funnel, loss ratio, premium trend, book of business).
    read:accounts:
      label: Read accounts
      group: reads
      description: Canonical insured account identity, addresses, classifications. Tenant-scoped via account_agency_relationships.
    clearance:
      label: Clearance pre-flight
      group: compute
      description: Submit a clearance check before filing a formal submission and read your own clearance request history. Other-party matches in the pre-flight check return only a boolean signal — no broker, name, or premium detail across tenants. (A blocking conflict at formal submission time returns the match identifiers needed for override/merge — see /create-submission.)
  presets:
    - id: indicative-pricing
      label: Indicative pricing
      description: |
        Stateless pricing math and pre-flight checks. Returns premium with an
        itemized modifier breakdown — but nothing persists, no quote
        number is issued, nothing is bindable. Useful for pre-appointment
        evaluation, partner sales tools, and indicative-pricing widgets. Not a
        firm quote in the carrier-binding sense.
      scopes: [rate, appetite, hazard]
    - id: read-only
      label: Read-only
      description: All read endpoints. Analytics, back-office dashboards, reconciliation jobs.
      scopes: [read:accounts, read:policies, read:submissions, read:quotes, read:claims, read:documents, read:reports]
    - id: brokerage-producer
      label: Brokerage producer
      description: |
        For wholesalers placing brokerage submissions (worked by Skadi underwriters,
        not auto-bound). Status checks, document retrieval, and indicative pricing
        to pre-screen risks before formal submission. No bind, no endorse — those
        operations don't apply to the brokerage channel. Submission intake itself
        is currently email-only; see /brokerage on the marketing site.
      scopes: [rate, appetite, hazard, read:accounts, read:policies, read:submissions, read:quotes, read:claims, read:documents, read:reports]
    - id: binding-authority
      label: Binding authority
      description: |
        For wholesalers with a Skadi letter of authority. The full quote-bind-issue-
        endorse-cancel-renew lifecycle, scoped to the firm's LoA. Out-of-authority
        risks are referred to Skadi's binding desk through the same flow. Standard
        package for broker AMS integrations. See /binding-authority on the
        marketing site.
      scopes: [rate, appetite, hazard, clearance, write:submissions, write:transactions, read:accounts, read:policies, read:submissions, read:quotes, read:claims, read:documents, read:reports]
  defaultPreset: indicative-pricing
  # Scopes that exist but are deliberately not offered to partners.
  internalOnly:
    - write:transactions:override

paths:

  # --------------------------------------------------------------------
  # Auth
  # --------------------------------------------------------------------
  /oauth-token:
    post:
      tags: [Auth]
      summary: Exchange API key + secret for a short-lived OAuth bearer.
      description: |
        RFC 6749 §4.4 Client Credentials grant. Returns a 1-hour token signed
        with HS256. Keys with `require_hmac=true` (enterprise / regulated)
        are refused at this endpoint and must use Mode A.
      operationId: oauthToken
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [grant_type, client_id, client_secret]
              properties:
                grant_type:    { type: string, enum: [client_credentials] }
                client_id:     { type: string, description: api_keys.id (UUID) }
                client_secret: { type: string, description: Raw API key (odn_live_... / odn_sb_...) }
                scope:         { type: string, description: Optional space-delimited subset of the key's scopes }
      responses:
        '200':
          description: Bearer token issued.
          content:
            application/json:
              schema:
                type: object
                required: [access_token, token_type, expires_in, scope]
                properties:
                  access_token: { type: string, description: HS256 JWT }
                  token_type:   { type: string, enum: [Bearer] }
                  expires_in:   { type: integer, description: Seconds until expiry, example: 3600 }
                  scope:        { type: string, description: Granted scopes (space-delimited) }
        '400': { $ref: '#/components/responses/Validation' }
        '401': { $ref: '#/components/responses/InvalidCredentials' }

  # --------------------------------------------------------------------
  # Rating
  # --------------------------------------------------------------------
  /rate-quote:
    post:
      tags: [Rating]
      summary: Compute premium for a tower structure without persisting.
      operationId: rateQuote
      x-skadi-scope: rate
      parameters:
        - $ref: '#/components/parameters/SkadiApiVersion'
        - $ref: '#/components/parameters/XRequestId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RateQuoteRequest' }
      responses:
        '200':
          description: Rated quote.
          headers:
            x-request-id:     { $ref: '#/components/headers/XRequestId' }
            Skadi-API-Version: { $ref: '#/components/headers/SkadiApiVersion' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RateQuoteResponse' }
        '400': { $ref: '#/components/responses/Validation' }
        '401': { $ref: '#/components/responses/InvalidCredentials' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '422': { $ref: '#/components/responses/Validation' }
        '429': { $ref: '#/components/responses/RateLimited' }

  # --------------------------------------------------------------------
  # Underwriting utilities
  # --------------------------------------------------------------------
  /appetite-check:
    get:
      tags: [Underwriting]
      summary: Score an opportunity against Skadi's appetite matrix.
      operationId: appetiteCheck
      x-skadi-scope: appetite
      parameters:
        - { name: naics,  in: query, schema: { type: string }, description: NAICS code (one of naics or state required) }
        - { name: state,  in: query, schema: { type: string }, description: 2-letter state }
        - { name: limit,  in: query, schema: { type: integer, format: int64 }, description: Tower limit }
        - { name: attachment, in: query, schema: { type: integer, format: int64 } }
        - $ref: '#/components/parameters/SkadiApiVersion'
      responses:
        '200':
          description: Appetite score.
          content:
            application/json:
              schema:
                type: object
                properties:
                  appetite:    { type: string, enum: [in, conditional, out] }
                  score:       { type: number }
                  reasons:     { type: array, items: { type: string } }
                  guidance:    { type: string }
        '400': { $ref: '#/components/responses/Validation' }
        '401': { $ref: '#/components/responses/InvalidCredentials' }

  /hazard-lookup:
    get:
      tags: [Underwriting]
      summary: Resolve a NAICS code to its hazard score and description.
      operationId: hazardLookup
      x-skadi-scope: hazard
      parameters:
        - { name: naics, in: query, required: true, schema: { type: string } }
        - $ref: '#/components/parameters/SkadiApiVersion'
      responses:
        '200':
          description: Hazard data.
          content:
            application/json:
              schema:
                type: object
                properties:
                  naics:       { type: string }
                  description: { type: string }
                  prem:        { type: integer }
                  prod:        { type: integer }
                  auto:        { type: integer }
                  liquor:      { type: integer }
        '404': { $ref: '#/components/responses/NotFound' }

  # --------------------------------------------------------------------
  # Clearance — pre-flight check + history
  # --------------------------------------------------------------------
  /clearance-request:
    post:
      tags: [Underwriting]
      summary: Submit a pre-flight clearance check.
      description: |
        Pre-flight a submission before filing it. Returns a redacted status
        — severity, suggested action, and (where applicable) detailed
        matches for accounts in **your own** agency book. Other-party
        matches collapse to a boolean signal: no broker name, no insured
        name, no premium history.

        Use the returned `request_id` on a follow-up `create-submission`
        call to link the cleared check to the formal submission for
        audit-trail continuity.

        Required documentation flags (`acord_application`, `loss_runs_5y`)
        are honor-system; partner attests in the body. Doc-repository
        validation is a separate audit track.
      operationId: clearanceRequest
      x-skadi-scope: clearance
      parameters:
        - $ref: '#/components/parameters/SkadiApiVersion'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [named_insured, state]
              properties:
                named_insured: { type: string, example: 'Acme Industries Inc.' }
                fein:          { type: string, example: '12-3456789', description: 'Optional but strongly recommended — exact-FEIN match drives Tier 1.' }
                state:         { type: string, example: 'TX' }
                naics:         { type: string, example: '722511' }
                zip:           { type: string, example: '75201' }
                window_days:   { type: integer, default: 90, minimum: 1, maximum: 365 }
                documentation:
                  type: object
                  description: Honor-system attestation of received documentation.
                  properties:
                    acord_application:          { type: boolean }
                    loss_runs_5y:               { type: boolean }
                    supplemental_questionnaire: { type: boolean }
      responses:
        '200':
          description: Clearance check result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  request_id:
                    type: string
                    format: uuid
                    description: Reference for downstream linkage on create-submission.
                  severity:
                    type: string
                    enum: [block, manual_review, advisory, clear]
                  suggested_action:
                    type: string
                    enum: [proceed, contact_uw, merge_with_own_submission]
                  overall_confidence:
                    type: number
                    minimum: 0
                    maximum: 1
                  documentation_complete: { type: boolean }
                  documentation_required: { type: array, items: { type: string } }
                  matched_in_partner_book:
                    type: array
                    description: |
                      Matches against THIS partner's own submission book — full
                      detail with per-hit disposition derived from the
                      M-tier × G-tier × same-agency × renewal-window matrix.
                    items:
                      $ref: '#/components/schemas/ClearanceMatchWithDisposition'
                  carrier_internal_match:
                    type: boolean
                    description: True iff a match exists outside the partner's own book. Identifying detail intentionally redacted.
                  carrier_internal_match_count:
                    type: integer
                  carrier_internal_match_summary:
                    type: object
                    nullable: true
                    description: |
                      Worst-case disposition across cross-tenant matches.
                      No PII or submission identifiers — only the structural
                      disposition so the partner knows what action the matched
                      record drives.
                    properties:
                      disposition:          { $ref: '#/components/schemas/Disposition' }
                      severity:             { $ref: '#/components/schemas/DispositionSeverity' }
                      label:                { type: string }
                      reasoning:            { type: string }
                      allowed_decisions:    { type: array, items: { $ref: '#/components/schemas/ClearanceDecision' } }
                      decision_authority:   { type: string, enum: [any_uw, admin] }
                      match_tier:           { type: string, enum: [M1, M2, M3, M4] }
                      effective_grade_code: { type: string, enum: [G1, G2, G3, B, X] }
        '400': { $ref: '#/components/responses/Validation' }
        '401': { $ref: '#/components/responses/InvalidCredentials' }
        '403': { $ref: '#/components/responses/InsufficientScope' }

  /clearance-requests:
    get:
      tags: [Underwriting]
      summary: List your own clearance requests.
      description: |
        Returns clearance requests filed by this partner, newest first.
        Includes status (severity), decisions if recorded by carrier,
        and linked submission ids. **Cross-tenant data is never returned**
        — partners see only their own clearance history.
      operationId: clearanceRequestList
      x-skadi-scope: clearance
      parameters:
        - { name: since,  in: query, schema: { type: string, format: date-time }, description: 'Only requests after this timestamp.' }
        - { name: status, in: query, schema: { type: string, enum: [cleared, advisory, requires_review, blocked] } }
        - { name: limit,  in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 50 } }
        - $ref: '#/components/parameters/SkadiApiVersion'
      responses:
        '200':
          description: Clearance request history.
          content:
            application/json:
              schema:
                type: object
                properties:
                  clearance_requests:
                    type: array
                    items:
                      type: object
                      properties:
                        id:                     { type: string, format: uuid }
                        submission_id:          { type: string, format: uuid, nullable: true }
                        query_named_insured:    { type: string }
                        query_fein:             { type: string, nullable: true }
                        query_state:            { type: string }
                        query_naics:            { type: string, nullable: true }
                        query_zip:              { type: string, nullable: true }
                        window_days:            { type: integer }
                        match_count:            { type: integer }
                        max_confidence:         { type: number, nullable: true }
                        severity:               { type: string, enum: [cleared, advisory, requires_review, blocked, block, manual_review, clear] }
                        documentation_received: { type: object }
                        decision:               { type: string, nullable: true, enum: [proceed, merge, block, override, null] }
                        decided_at:             { type: string, format: date-time, nullable: true }
                        merged_into_submission_id: { type: string, format: uuid, nullable: true }
                        created_at:             { type: string, format: date-time }
                  count:    { type: integer }
                  has_more: { type: boolean }
        '401': { $ref: '#/components/responses/InvalidCredentials' }
        '403': { $ref: '#/components/responses/InsufficientScope' }

  /clearance-match-resolve:
    post:
      tags: [Underwriting]
      summary: Record a per-hit clearance match resolution.
      description: |
        Persist a per-hit resolution against a `clearance_match` returned by
        `POST /clearance-request` (look at `matched_in_partner_book[*]`).
        Multi-hit clearance results carry per-hit resolutions — match #1
        can be `merged` while match #2 is `linked_related`.

        Idempotent: the same `Idempotency-Key` returns the same response.
        Different body with the same key returns `409 idempotency-conflict`.
        Replaying with the same body returns the original resolution row
        (`ON CONFLICT (clearance_match_id) DO NOTHING` server-side).

        Resolution kinds:
          * `merged`                          — sets `submissions.merged_into_submission_id` on the merge-from row when the match originated from a draft. Returns `redirect_submission_id`.
          * `opened_existing`                 — discards new draft + resumes original. Returns `redirect_submission_id`.
          * `linked_related`                  — peer cross-reference; both records preserved.
          * `route_renewal`                   — returns `redirect_policy_id` so caller can kick the renewal flow with `prior_policy_id` seeded.
          * `not_related`                     — **admin-only**. Note required (≥ 1 char).
          * `cleared_release`                 — **admin-only**. Note required.
          * `cleared_renewal_authorization`   — **admin-only**. Note required.
          * `cleared_indication`              — audit-only.
          * `blocking_pending`                — holds the match in pending state.
      operationId: clearanceMatchResolve
      x-skadi-scope: clearance
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/SkadiApiVersion'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [match_id, resolution]
              properties:
                match_id:
                  type: string
                  format: uuid
                  description: id of the `clearance_match` to resolve.
                resolution:
                  type: string
                  enum:
                    - merged
                    - opened_existing
                    - linked_related
                    - route_renewal
                    - not_related
                    - cleared_release
                    - cleared_renewal_authorization
                    - cleared_indication
                    - blocking_pending
                note:
                  type: string
                  nullable: true
                  description: Required for `not_related`, `cleared_release`, `cleared_renewal_authorization`. Free-text reasoning for E&O audit.
      responses:
        '200':
          description: Resolution recorded (or replayed).
          content:
            application/json:
              schema:
                type: object
                required: [resolution_id, clearance_match_id, resolution, resolved_by, resolved_at]
                properties:
                  resolution_id:          { type: string, format: uuid }
                  clearance_match_id:     { type: string, format: uuid }
                  resolution:             { type: string }
                  resolved_by:            { type: string, description: 'User id (internal) or api_key id (partner).' }
                  resolved_at:            { type: string, format: date-time }
                  note:                   { type: string, nullable: true }
                  redirect_submission_id: { type: string, format: uuid, nullable: true, description: 'Set for merged / opened_existing.' }
                  redirect_policy_id:     { type: string, format: uuid, nullable: true, description: 'Set for route_renewal.' }
        '400': { $ref: '#/components/responses/Validation' }
        '401': { $ref: '#/components/responses/InvalidCredentials' }
        '403':
          description: Match is outside your tenant scope OR admin-only kind from a non-admin key.
          content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: Match already has a resolution recorded with a different kind, or `Idempotency-Key` collision.
          content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
        '422':
          description: Note required for E&O kinds (`not_related`, `cleared_release`, `cleared_renewal_authorization`).
          content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }

  # --------------------------------------------------------------------
  # Submissions
  # --------------------------------------------------------------------
  /create-submission:
    post:
      tags: [Submissions]
      summary: File a new submission for downstream quote/bind.
      operationId: createSubmission
      x-skadi-scope: write:submissions
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/SkadiApiVersion'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [account, requested_towers]
              properties:
                account:         { $ref: '#/components/schemas/Account' }
                requested_towers:
                  type: array
                  items: { $ref: '#/components/schemas/Tower' }
                broker_email:    { type: string, format: email }
      responses:
        '201':
          description: Submission accepted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  submission_id:     { type: string, format: uuid }
                  submission_number: { type: string }
        '400': { $ref: '#/components/responses/Validation' }
        '409': { $ref: '#/components/responses/IdempotencyConflict' }

  # --------------------------------------------------------------------
  # Transactions — quote-then-bind family.
  # All resources share the same lifecycle:
  #   POST /resource           → priced draft
  #   GET  /resource/{id}      → readback
  #   PATCH /resource/{id}     → re-rate (where supported)
  #   DELETE /resource/{id}    → abandon
  #   POST /resource/{id}/bind → commit (idempotent)
  #
  # Resources: endorsements, cancellations, extensions, audits, renewals,
  # rewrites, oos-rebases. Below we document endorsements in full as the
  # canonical shape, then list the others as paths-only with refs.
  #
  # /process-transaction is the single-endpoint path retained for the
  # deterministic state-flip kinds (reinstatement + non_renewal) that
  # don't need a draft round-trip. Rating-affected kinds were moved to
  # dedicated resources and return 410 here pointing at their
  # replacement via extensions.redirect_endpoint.
  # --------------------------------------------------------------------
  /process-transaction:
    post:
      tags: [Transactions]
      summary: Apply a deterministic state-flip transaction (reinstatement / non_renewal).
      description: "Single-shot endpoint for state-flip kinds that don't involve rating. Calls with rating-affected kinds (endorsement, cancellation, extension, audit, renewal, rewrite) return 410 with an extensions.redirect_endpoint hint."
      operationId: processTransaction
      x-skadi-scope: write:transactions
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/SkadiApiVersion'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [policy_number, transaction_kind, effective_date]
              properties:
                policy_number:    { type: string }
                transaction_kind: { type: string, enum: [reinstatement, non_renewal] }
                effective_date:   { type: string, format: date }
                description:      { type: string }
      responses:
        '200':
          description: Applied.
          content:
            application/json:
              schema:
                type: object
                properties:
                  transaction_id:   { type: string, format: uuid }
                  policy_id:        { type: string, format: uuid }
                  policy_status:    { type: string }
                  effective_date:   { type: string, format: date }
                  sequence_number:  { type: integer }
        '400': { $ref: '#/components/responses/Validation' }
        '401': { $ref: '#/components/responses/InvalidCredentials' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '409': { $ref: '#/components/responses/Validation' }
        '410':
          description: Rating-affected kind — use the dedicated resource. Body's `extensions.redirect_endpoint` points at the right path.
          content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }

  /endorsements:
    post:
      tags: [Transactions]
      summary: Create a priced endorsement draft.
      operationId: createEndorsementDraft
      x-skadi-scope: write:transactions
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/SkadiApiVersion'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EndorsementCreate' }
      responses:
        '201':
          description: Draft created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EndorsementDraft' }
        '400': { $ref: '#/components/responses/Validation' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/IdempotencyConflict' }
  /endorsements/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [Transactions]
      summary: Read an endorsement draft.
      operationId: getEndorsementDraft
      x-skadi-scope: read:policies
      responses:
        '200':
          description: Draft.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EndorsementDraft' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Transactions]
      summary: Re-rate a draft with new changes.
      operationId: patchEndorsementDraft
      x-skadi-scope: write:transactions
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                changes:     { type: array, items: { $ref: '#/components/schemas/EndorsementOp' } }
                description: { type: string }
      responses:
        '200':
          description: Re-rated draft.
          content: { application/json: { schema: { $ref: '#/components/schemas/EndorsementDraft' } } }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Transactions]
      summary: Abandon a draft.
      operationId: abandonEndorsementDraft
      x-skadi-scope: write:transactions
      responses:
        '204': { description: Abandoned. }
        '404': { $ref: '#/components/responses/NotFound' }
  /endorsements/{id}/bind:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      - $ref: '#/components/parameters/IdempotencyKey'
    post:
      tags: [Transactions]
      summary: Bind a previously-priced endorsement draft.
      operationId: bindEndorsementDraft
      x-skadi-scope: write:transactions
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                charged: { $ref: '#/components/schemas/VarianceCharged' }
      responses:
        '200':
          description: Bound.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EndorsementDraft'
                  - type: object
                    properties:
                      transaction_id: { type: string, format: uuid }
        '400': { $ref: '#/components/responses/Validation' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '409': { $ref: '#/components/responses/StaleSnapshot' }
        '410': { $ref: '#/components/responses/DraftExpired' }
        '422': { $ref: '#/components/responses/ScenarioNotBindable' }
  /endorsements/bulk:
    post:
      tags: [Transactions]
      summary: Create+bind up to 50 endorsements in one request (partial-success report).
      description: |
        Each item is create-then-bind, processed independently — the response
        is a partial-success report (`succeeded[]` / `failed[]`), not
        all-or-nothing. A per-item `idempotency_key` (required) threads through
        draft creation and bind, so replays short-circuit at commit and never
        double-bind. Max 50 items.
      operationId: bulkEndorsements
      x-skadi-scope: write:transactions
      parameters:
        - $ref: '#/components/parameters/SkadiApiVersion'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [items]
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 50
                  items:
                    type: object
                    required: [idempotency_key, policy_id, effective_date, changes]
                    properties:
                      idempotency_key: { type: string, maxLength: 255 }
                      policy_id:       { type: string, format: uuid }
                      effective_date:  { type: string, format: date }
                      description:     { type: [string, "null"] }
                      changes:         { type: array, minItems: 1, items: { $ref: '#/components/schemas/EndorsementOp' } }
      responses:
        '200':
          description: Partial-success report. Each item bound or reported as failed independently.
          content:
            application/json:
              schema:
                type: object
                properties:
                  succeeded:
                    type: array
                    items:
                      type: object
                      properties:
                        item_index:     { type: integer }
                        endorsement_id: { type: string, format: uuid }
                        transaction_id: { type: string, format: uuid }
                  failed:
                    type: array
                    items:
                      type: object
                      properties:
                        item_index: { type: integer }
                        error:      { $ref: '#/components/schemas/Problem' }
        '400': { $ref: '#/components/responses/Validation' }
        '403': { $ref: '#/components/responses/InsufficientScope' }

  # Cancellations / extensions / audits / renewals / rewrites / oos_rebases
  # all follow the same lifecycle as /endorsements above. Resource-specific
  # request/response payloads are described on the developer portal's
  # Lifecycles page; the YAML below documents the operation surface +
  # scope + error catalog.
  /cancellations:
    post:
      tags: [Transactions]
      summary: Create a priced cancellation draft.
      description: Same lifecycle as `/endorsements`. Body carries `policy_id`, `cancellation_date`, `method` (`pro_rata` | `short_rate`), `reason_code`, optional `notice_date`.
      operationId: createCancellationDraft
      x-skadi-scope: write:transactions
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CancellationCreate' }
      responses:
        '201':
          description: Draft created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CancellationDraft' }
        '400': { $ref: '#/components/responses/Validation' }
  /cancellations/{id}:
    parameters: [{ name: id, in: path, required: true, schema: { type: string, format: uuid } }]
    get:
      tags: [Transactions]
      summary: Read a cancellation draft.
      operationId: getCancellationDraft
      x-skadi-scope: read:policies
      responses:
        '200': { description: Draft., content: { application/json: { schema: { type: object, additionalProperties: true } } } }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Transactions]
      summary: Abandon a cancellation draft.
      operationId: abandonCancellationDraft
      x-skadi-scope: write:transactions
      responses:
        '204': { description: Abandoned. }
        '404': { $ref: '#/components/responses/NotFound' }
  /cancellations/{id}/bind:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      - $ref: '#/components/parameters/IdempotencyKey'
    post:
      tags: [Transactions]
      summary: Bind a cancellation draft.
      operationId: bindCancellationDraft
      x-skadi-scope: write:transactions
      responses:
        '200':
          description: Bound.
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/CancellationDraft' }
                  - { $ref: '#/components/schemas/BoundTransactionId' }
        '409': { $ref: '#/components/responses/StaleSnapshot' }
        '410': { $ref: '#/components/responses/DraftExpired' }
        '422': { $ref: '#/components/responses/ScenarioNotBindable' }

  /extensions:
    post:
      tags: [Transactions]
      summary: Create a priced extension draft.
      description: Same lifecycle as `/endorsements`. Body carries `policy_id` and `extended_expiration`.
      operationId: createExtensionDraft
      x-skadi-scope: write:transactions
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ExtensionCreate' }
      responses:
        '201':
          description: Draft created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ExtensionDraft' }
        '400': { $ref: '#/components/responses/Validation' }
  /extensions/{id}:
    parameters: [{ name: id, in: path, required: true, schema: { type: string, format: uuid } }]
    get:
      tags: [Transactions]
      summary: Read an extension draft.
      operationId: getExtensionDraft
      x-skadi-scope: read:policies
      responses:
        '200': { description: Draft., content: { application/json: { schema: { type: object, additionalProperties: true } } } }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Transactions]
      summary: Abandon an extension draft.
      operationId: abandonExtensionDraft
      x-skadi-scope: write:transactions
      responses:
        '204': { description: Abandoned. }
        '404': { $ref: '#/components/responses/NotFound' }
  /extensions/{id}/bind:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      - $ref: '#/components/parameters/IdempotencyKey'
    post:
      tags: [Transactions]
      summary: Bind an extension draft.
      operationId: bindExtensionDraft
      x-skadi-scope: write:transactions
      responses:
        '200':
          description: Bound.
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/ExtensionDraft' }
                  - { $ref: '#/components/schemas/BoundTransactionId' }
        '409': { $ref: '#/components/responses/StaleSnapshot' }
        '410': { $ref: '#/components/responses/DraftExpired' }
        '422': { $ref: '#/components/responses/ScenarioNotBindable' }

  /audits:
    post:
      tags: [Transactions]
      summary: Create a priced audit draft.
      description: Same lifecycle as `/endorsements`. Body carries `policy_id`, `audit_period_start`, `audit_period_end`, `exposures`.
      operationId: createAuditDraft
      x-skadi-scope: write:transactions
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AuditCreate' }
      responses:
        '201':
          description: Draft created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AuditDraft' }
        '400': { $ref: '#/components/responses/Validation' }
  /audits/{id}:
    parameters: [{ name: id, in: path, required: true, schema: { type: string, format: uuid } }]
    get:
      tags: [Transactions]
      summary: Read an audit draft.
      operationId: getAuditDraft
      x-skadi-scope: read:policies
      responses:
        '200': { description: Draft., content: { application/json: { schema: { type: object, additionalProperties: true } } } }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Transactions]
      summary: Abandon an audit draft.
      operationId: abandonAuditDraft
      x-skadi-scope: write:transactions
      responses:
        '204': { description: Abandoned. }
        '404': { $ref: '#/components/responses/NotFound' }
  /audits/{id}/bind:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      - $ref: '#/components/parameters/IdempotencyKey'
    post:
      tags: [Transactions]
      summary: Bind an audit draft.
      operationId: bindAuditDraft
      x-skadi-scope: write:transactions
      responses:
        '200':
          description: Bound.
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/AuditDraft' }
                  - { $ref: '#/components/schemas/BoundTransactionId' }
        '409': { $ref: '#/components/responses/StaleSnapshot' }
        '410': { $ref: '#/components/responses/DraftExpired' }
        '422': { $ref: '#/components/responses/ScenarioNotBindable' }

  /renewals:
    post:
      tags: [Transactions]
      summary: Create a priced renewal draft.
      description: Same lifecycle as `/endorsements`. Body carries `policy_id`, `new_effective_date`, optional `new_expiration_date`, optional `changes` array.
      operationId: createRenewalDraft
      x-skadi-scope: write:transactions
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RenewalCreate' }
      responses:
        '201':
          description: Draft created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RenewalDraft' }
        '400': { $ref: '#/components/responses/Validation' }
  /renewals/{id}:
    parameters: [{ name: id, in: path, required: true, schema: { type: string, format: uuid } }]
    get:
      tags: [Transactions]
      summary: Read a renewal draft.
      operationId: getRenewalDraft
      x-skadi-scope: read:policies
      responses:
        '200': { description: Draft., content: { application/json: { schema: { type: object, additionalProperties: true } } } }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Transactions]
      summary: Abandon a renewal draft.
      operationId: abandonRenewalDraft
      x-skadi-scope: write:transactions
      responses:
        '204': { description: Abandoned. }
        '404': { $ref: '#/components/responses/NotFound' }
  /renewals/{id}/bind:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      - $ref: '#/components/parameters/IdempotencyKey'
    post:
      tags: [Transactions]
      summary: Bind a renewal draft.
      operationId: bindRenewalDraft
      x-skadi-scope: write:transactions
      responses:
        '200':
          description: Bound.
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/RenewalDraft' }
                  - { $ref: '#/components/schemas/BoundTransactionId' }
        '409': { $ref: '#/components/responses/StaleSnapshot' }
        '410': { $ref: '#/components/responses/DraftExpired' }
        '422': { $ref: '#/components/responses/ScenarioNotBindable' }

  /rewrites:
    post:
      tags: [Transactions]
      summary: Create a priced rewrite draft.
      description: Same lifecycle as `/endorsements`. Body carries `policy_id`, `rewrite_date`, optional `new_named_insured`, optional `changes` array.
      operationId: createRewriteDraft
      x-skadi-scope: write:transactions
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RewriteCreate' }
      responses:
        '201':
          description: Draft created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RewriteDraft' }
        '400': { $ref: '#/components/responses/Validation' }
  /rewrites/{id}:
    parameters: [{ name: id, in: path, required: true, schema: { type: string, format: uuid } }]
    get:
      tags: [Transactions]
      summary: Read a rewrite draft.
      operationId: getRewriteDraft
      x-skadi-scope: read:policies
      responses:
        '200': { description: Draft., content: { application/json: { schema: { type: object, additionalProperties: true } } } }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Transactions]
      summary: Abandon a rewrite draft.
      operationId: abandonRewriteDraft
      x-skadi-scope: write:transactions
      responses:
        '204': { description: Abandoned. }
        '404': { $ref: '#/components/responses/NotFound' }
  /rewrites/{id}/bind:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      - $ref: '#/components/parameters/IdempotencyKey'
    post:
      tags: [Transactions]
      summary: Bind a rewrite draft.
      operationId: bindRewriteDraft
      x-skadi-scope: write:transactions
      responses:
        '200':
          description: Bound.
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/RewriteDraft' }
                  - { $ref: '#/components/schemas/BoundTransactionId' }
        '409': { $ref: '#/components/responses/StaleSnapshot' }
        '410': { $ref: '#/components/responses/DraftExpired' }
        '422': { $ref: '#/components/responses/ScenarioNotBindable' }

  /oos-rebases:
    post:
      tags: [Transactions]
      summary: Create a priced out-of-sequence rebase draft.
      description: Same lifecycle as `/endorsements`. Body carries `policy_id`, `oos_effective_date`, optional `oos_changes`. See the developer portal's Lifecycles page for the void-offset-replace contract.
      operationId: createOosRebaseDraft
      x-skadi-scope: write:transactions
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/OosRebaseCreate' }
      responses:
        '201':
          description: Draft created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OosRebaseDraft' }
        '400': { $ref: '#/components/responses/Validation' }
  /oos-rebases/{id}:
    parameters: [{ name: id, in: path, required: true, schema: { type: string, format: uuid } }]
    get:
      tags: [Transactions]
      summary: Read an OOS rebase draft.
      operationId: getOosRebaseDraft
      x-skadi-scope: read:policies
      responses:
        '200': { description: Draft., content: { application/json: { schema: { type: object, additionalProperties: true } } } }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Transactions]
      summary: Abandon an OOS rebase draft.
      operationId: abandonOosRebaseDraft
      x-skadi-scope: write:transactions
      responses:
        '204': { description: Abandoned. }
        '404': { $ref: '#/components/responses/NotFound' }
  /oos-rebases/{id}/bind:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      - $ref: '#/components/parameters/IdempotencyKey'
    post:
      tags: [Transactions]
      summary: Bind an OOS rebase draft.
      operationId: bindOosRebaseDraft
      x-skadi-scope: write:transactions
      responses:
        '200':
          description: Bound.
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/OosRebaseDraft' }
                  - { $ref: '#/components/schemas/BoundTransactionId' }
        '409': { $ref: '#/components/responses/StaleSnapshot' }
        '410': { $ref: '#/components/responses/DraftExpired' }
        '422': { $ref: '#/components/responses/ScenarioNotBindable' }

  # --------------------------------------------------------------------
  # Reads
  # --------------------------------------------------------------------
  /get-accounts:
    get:
      tags: [Reads]
      summary: List or fetch insured accounts.
      description: |
        Returns canonical insured account identity rows (named insured, FEIN,
        DBAs, NAICS, primary address, ops description). Multi-tenant: partner
        keys see only accounts that have at least one agency relationship in
        their agency_group, including historical (BOR'd-away) ones.
      operationId: getAccounts
      x-skadi-scope: read:accounts
      parameters:
        - { name: id,                  in: query, schema: { type: string, format: uuid }, description: Single-account fetch by id }
        - { name: naics,               in: query, schema: { type: string }, description: Exact NAICS filter (full 6 digits) }
        - { name: state,               in: query, schema: { type: string }, description: 2-letter primary state }
        - { name: named_insured,       in: query, schema: { type: string }, description: Partial ilike match on named_insured }
        - { name: updated_at_gte,      in: query, schema: { type: string, format: date-time } }
        - { name: updated_at_lte,      in: query, schema: { type: string, format: date-time } }
        - { name: limit,               in: query, schema: { type: integer, default: 25, maximum: 200 } }
        - { name: cursor,              in: query, schema: { type: string }, description: Opaque cursor from a prior page's next_cursor }
        - { name: sort,                in: query, schema: { type: string, enum: [updated_at_desc, updated_at_asc, named_insured_asc, named_insured_desc] } }
      responses:
        '200':
          description: List or single account.
          content:
            application/json:
              schema:
                oneOf:
                  - { type: object, properties: { account: { $ref: '#/components/schemas/InsuredAccount' } } }
                  - type: object
                    properties:
                      accounts:    { type: array, items: { $ref: '#/components/schemas/InsuredAccount' } }
                      count:       { type: integer }
                      next_cursor: { type: string, nullable: true }
        '400': { $ref: '#/components/responses/Validation' }
        '401': { $ref: '#/components/responses/InvalidCredentials' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }

  /get-policies:
    get:
      tags: [Reads]
      summary: List or fetch policies.
      operationId: getPolicies
      x-skadi-scope: read:policies
      parameters:
        - { name: id,                  in: query, schema: { type: string, format: uuid } }
        - { name: status,              in: query, schema: { type: string, enum: [bound, in_force, pending_cancel, cancelled, pending_non_renew, non_renewed, expired, renewed, rewritten] } }
        - { name: effective_date_gte,  in: query, schema: { type: string, format: date } }
        - { name: effective_date_lte,  in: query, schema: { type: string, format: date } }
        - { name: limit,               in: query, schema: { type: integer, default: 25, maximum: 100 } }
        - { name: offset,              in: query, schema: { type: integer, default: 0 } }
        - { name: sort,                in: query, schema: { type: string, enum: [effective_date_desc, effective_date_asc] } }
      responses:
        '200':
          description: List or single policy.
          content:
            application/json:
              schema:
                oneOf:
                  - { type: object, properties: { policy: { $ref: '#/components/schemas/Policy' } } }
                  - { type: array, items: { $ref: '#/components/schemas/Policy' } }
        '404': { $ref: '#/components/responses/NotFound' }

  /get-submissions:
    get:
      tags: [Reads]
      summary: List or fetch submissions.
      operationId: getSubmissions
      x-skadi-scope: read:submissions
      responses:
        '200':
          description: List.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Submission' }
        '400': { $ref: '#/components/responses/Validation' }
        '401': { $ref: '#/components/responses/InvalidCredentials' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }
  /get-quotes:
    get:
      tags: [Reads]
      summary: List or fetch quotes.
      operationId: getQuotes
      x-skadi-scope: read:quotes
      responses:
        '200':
          description: List.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Quote' }
        '400': { $ref: '#/components/responses/Validation' }
        '401': { $ref: '#/components/responses/InvalidCredentials' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }
  /get-claims:
    get:
      tags: [Reads]
      summary: List or fetch claims.
      operationId: getClaims
      x-skadi-scope: read:claims
      responses:
        '200':
          description: List.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Claim' }
        '400': { $ref: '#/components/responses/Validation' }
        '401': { $ref: '#/components/responses/InvalidCredentials' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }
  /get-documents:
    get:
      tags: [Reads]
      summary: List documents (metadata only).
      description: Forms and document metadata for a policy. No file bytes — metadata only.
      operationId: getDocuments
      x-skadi-scope: read:documents
      responses:
        '200':
          description: List.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Document' }
        '400': { $ref: '#/components/responses/Validation' }
        '401': { $ref: '#/components/responses/InvalidCredentials' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }
  /get-policy-transactions:
    get:
      tags: [Reads]
      summary: List policy transactions.
      operationId: getPolicyTransactions
      x-skadi-scope: read:policies
      responses:
        '200':
          description: List.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/PolicyTransaction' }
        '400': { $ref: '#/components/responses/Validation' }
        '401': { $ref: '#/components/responses/InvalidCredentials' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404': { $ref: '#/components/responses/NotFound' }

  /get-reports:
    get:
      tags: [Reports]
      summary: Tenant-scoped aggregated report rows.
      operationId: getReports
      x-skadi-scope: read:reports
      parameters:
        - { name: type, in: query, required: true, schema: { type: string, enum: [broker_scorecard, submission_funnel, book_of_business, loss_ratio_monthly, premium_trend_monthly] } }
        - { name: start_date, in: query, schema: { type: string, format: date } }
        - { name: end_date,   in: query, schema: { type: string, format: date } }
      responses:
        '200':
          description: Report rows.
          content:
            application/json:
              schema:
                type: array
                items: { type: object, additionalProperties: true }
        '400': { $ref: '#/components/responses/Validation' }
        '401': { $ref: '#/components/responses/InvalidCredentials' }
        '403': { $ref: '#/components/responses/InsufficientScope' }

  # --------------------------------------------------------------------
  # System
  # --------------------------------------------------------------------
  /health-check:
    get:
      tags: [System]
      summary: Liveness + version probe.
      operationId: healthCheck
      security: []
      responses:
        '200':
          description: Healthy.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:         { type: boolean }
                  api_version: { type: string }
        '405':
          description: Method not allowed. Health-check is GET-only.
          content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
        '503':
          description: Service unavailable. Returned when one or more upstream dependencies (DB, Edge runtime) is unhealthy.
          content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }

  # --------------------------------------------------------------------
  # Documents (Vercel-app host, not Edge Functions)
  # --------------------------------------------------------------------
  /api/partner/quote-pdf:
    servers:
      - url: https://app.skadispecialty.com
        description: Vercel app host — serves live and sandbox; the API key prefix (odn_live_* / odn_sb_*) selects the environment.
    post:
      tags: [Documents]
      summary: Render a quote / binder / indication as PDF or ACORD JSON.
      description: |
        Content-negotiated by `Accept`:
        - `application/pdf` (default) → rendered PDF binary with embedded
          JSON sidecar (PDF 1.7 `EmbeddedFile`). The sidecar's
          `contentHash` matches the PDF's `/ID` array.
        - `application/json` → ACORD envelope (ACORD 137 for
          `quote`/`indication`, ACORD 75 for `binder`).

        A successful render emits a `document.generated` webhook event.
      operationId: renderPartnerQuotePdf
      x-skadi-scope: write:documents
      parameters:
        - name: Accept
          in: header
          required: true
          schema: { type: string, enum: ['application/pdf', 'application/json'] }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [quoteId]
              properties:
                quoteId:      { type: string, format: uuid }
                documentType: { type: string, enum: [quote, binder, indication], default: quote }
      responses:
        '200':
          description: Rendered document.
          content:
            application/pdf:
              schema: { type: string, format: binary }
            application/json:
              schema:
                type: object
                description: ACORD envelope. Shape documented at `api/_lib/acordExport.ts`. `shapeVersion 1.0.0`.
                additionalProperties: true
        '400': { $ref: '#/components/responses/Validation' }
        '401': { $ref: '#/components/responses/InvalidCredentials' }
        '403': { $ref: '#/components/responses/InsufficientScope' }
        '404':
          description: Quote not found or not in scope for this partner key.
          content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
        '405':
          description: Method not allowed. POST only.
          content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
        '406':
          description: Unsupported `Accept` header. Use `application/pdf` or `application/json`.
          content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }

# ------------------------------------------------------------------------
# Components
# ------------------------------------------------------------------------
components:

  securitySchemes:
    hmacKey:
      type: apiKey
      name: X-API-Key
      in: header
      description: |
        HMAC mode (preferred). Send `X-API-Key`, `X-Timestamp` (Unix seconds),
        and `X-Signature: sha256=<hex>` where the MAC is computed over
        `${timestamp}.${rawBody}` using your HMAC secret as the key.

        The canonical-string format is a stable contract; if your team
        needs SigV4 / `Stripe-Signature` compatibility, contact partner
        engineering and we'll add a shim rather than break the canonical
        string.

        Requests whose timestamp is more than ±5 minutes from server time
        are rejected with `401 timestamp-skew`.
    oauthBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        OAuth 2.0 Client Credentials. Exchange your `api_key.id` (as
        client_id) plus raw key (as client_secret) at `/oauth-token` for
        a 1-hour HS256 JWT. Present as `Authorization: Bearer <token>`.

        Keys with `require_hmac=true` are refused at the token endpoint
        and must use HMAC.

  parameters:
    SkadiApiVersion:
      name: Skadi-API-Version
      in: header
      required: false
      schema: { type: string, example: '2026-04-25' }
      description: Pin a specific API version. Defaults to latest.
    XRequestId:
      name: X-Request-Id
      in: header
      required: false
      schema: { type: string, format: uuid }
      description: Caller-supplied correlation id. Echoed in response headers + audit log.
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string, maxLength: 255 }
      description: |
        Replay-safe key. Same key + same body returns the original response;
        same key + different body returns `409 idempotency-conflict`.

  headers:
    XRequestId:
      schema: { type: string, format: uuid }
      description: Correlation id, echoed for log alignment.
    SkadiApiVersion:
      schema: { type: string }
      description: Resolved API version that served this response.
    RetryAfter:
      schema: { type: integer }
      description: Seconds to wait before retry (rate limit).

  responses:
    Validation:
      description: Request validation failed.
      content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
    InvalidCredentials:
      description: Missing, malformed, expired, or wrong-tenant credentials.
      content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
    InsufficientScope:
      description: Authentication succeeded but scope is missing.
      content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
    NotFound:
      description: Resource does not exist or is outside the caller's tenant scope.
      content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
    RateLimited:
      description: Rate limit exceeded (per-key or per-IP).
      headers: { Retry-After: { $ref: '#/components/headers/RetryAfter' } }
      content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
    IdempotencyConflict:
      description: Idempotency key reused with a different body.
      content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
    StaleSnapshot:
      description: Policy state changed since the draft was created. Body includes `fresh_draft`.
      content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
    DraftExpired:
      description: Draft TTL passed. Create a new one.
      content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
    ScenarioNotBindable:
      description: Draft was created with `scenario_only=true` and cannot be bound.
      content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }

  schemas:

    Problem:
      type: object
      description: RFC 7807 Problem Details.
      properties:
        type:     { allOf: [{ $ref: '#/components/schemas/ProblemType' }] }
        title:    { type: string }
        status:   { type: integer }
        detail:   { type: string }
        instance: { type: string }
      required: [type, title, status]

    ProblemType:
      description: Catalog of `type` values you may receive.
      type: string
      enum:
        - https://skadispecialty.com/api/problems/missing-credentials
        - https://skadispecialty.com/api/problems/invalid-credentials
        - https://skadispecialty.com/api/problems/expired-credentials
        - https://skadispecialty.com/api/problems/insufficient-scope
        - https://skadispecialty.com/api/problems/invalid-signature
        - https://skadispecialty.com/api/problems/timestamp-skew
        - https://skadispecialty.com/api/problems/idempotency-conflict
        - https://skadispecialty.com/api/problems/rate-limit-exceeded
        - https://skadispecialty.com/api/problems/validation-failed
        - https://skadispecialty.com/api/problems/not-found
        - https://skadispecialty.com/api/problems/method-not-allowed
        - https://skadispecialty.com/api/problems/gone
        - https://skadispecialty.com/api/problems/stale-snapshot
        - https://skadispecialty.com/api/problems/draft-expired
        - https://skadispecialty.com/api/problems/scenario-not-bindable
        - https://skadispecialty.com/api/problems/internal-error

    # Clearance disposition matrix
    Disposition:
      type: string
      description: |
        Per-hit disposition derived from the M-tier × G-tier × same-agency ×
        renewal-window matrix.
      enum:
        - cleared
        - duplicate_of
        - blocked
        - warn_review
        - inform
        - advisory
        - renewal_opportunity
        - renewal_workflow
        - expired_prior

    DispositionSeverity:
      type: string
      enum: [block, manual_review, advisory, info, clear]

    ClearanceDecision:
      type: string
      enum: [proceed, merge, block, override, route_renewal]

    ClearanceMatchWithDisposition:
      type: object
      description: |
        A single clearance hit with its computed disposition. Returned only
        for the partner's own matches (cross-tenant matches collapse to
        carrier_internal_match_summary).
      properties:
        match_kind:              { type: string, enum: [exact_fein, name_state, fuzzy_trigram, address_zip] }
        match_tier:              { type: string, enum: [M1, M2, M3, M4] }
        confidence:              { type: number, minimum: 0, maximum: 1 }
        matched_submission_id:   { type: string, format: uuid }
        matched_named_insured:   { type: string }
        matched_state:           { type: string, nullable: true }
        matched_outcome:         { type: string, nullable: true }
        matched_at:              { type: string, format: date-time }
        matched_grade:           { type: string, enum: [tier_1, tier_2, tier_3], nullable: true }
        matched_expiration_date: { type: string, format: date, nullable: true, description: 'Populated when the match resolves to a bound policy.' }
        within_window:           { type: boolean }
        same_agency:             { type: boolean }
        days_to_expiry:          { type: integer, nullable: true, description: 'Days until matched policy expires; negative = past. Bound matches only.' }
        disposition:             { $ref: '#/components/schemas/Disposition' }
        severity:                { $ref: '#/components/schemas/DispositionSeverity' }
        label:                   { type: string }
        reasoning:               { type: string, description: 'One-sentence WHY for the disposition.' }
        allowed_decisions:       { type: array, items: { $ref: '#/components/schemas/ClearanceDecision' } }
        decision_authority:      { type: string, enum: [any_uw, admin] }
        effective_grade:         { type: string, enum: [tier_1, tier_2, tier_3, bound, expired] }
        effective_grade_code:    { type: string, enum: [G1, G2, G3, B, X] }
      required:
        - match_kind
        - match_tier
        - confidence
        - matched_named_insured
        - matched_at
        - within_window
        - same_agency
        - disposition
        - severity
        - label
        - allowed_decisions
        - decision_authority

    Account:
      type: object
      properties:
        named_insured:    { type: string }
        naics_code:       { type: string, description: NAICS classification code }
        address_state:    { type: string, minLength: 2, maxLength: 2 }
        effective_date:   { type: string, format: date }
        expiration_date:  { type: string, format: date }
        # Exposure normalization. All optional; used to bucket comparable
        # risks into log-scale revenue bands per Marsh / Aon / WTW 2025
        # conventions.
        annual_revenue:   { type: number, format: double, description: Insured's annual revenue in USD. Universal exposure proxy across all casualty lines per public broker market reports. }
        fte_count:        { type: integer, format: int32, description: Full-time-equivalent headcount. Workforce exposure proxy for products + premises liability. }
        fleet_size:       { type: integer, format: int32, description: Vehicle fleet size. Required exposure normalizer for AUTO line. }
        tiv:              { type: number, format: double, description: Total Insured Value in USD. Property-adjacent casualty exposure proxy (rarely cited on pure casualty placements). }

    Tower:
      type: object
      properties:
        id:             { type: string, format: uuid, readOnly: true }
        lineType:       { type: string, enum: [GL, AUTO, LIQUOR] }
        attachment:     { type: integer, format: int64, description: Attachment in dollars }
        limit:          { type: integer, format: int64, description: Limit in dollars }
        primaryLimit:   { type: integer, format: int64 }
        primaryCarrier: { type: string }
        premPremium:    { type: number }
        prodPremium:    { type: number }
        # Exposure normalization. Underlying primary GL premium is
        # the gold-standard cross-NAICS exposure normalizer for excess
        # casualty per Marsh excess-tower benchmarks — primary carrier
        # already encoded hazard + exposure + losses into one number.
        underlyingPrimaryPremium: { type: number, format: double, description: Underlying primary policy premium in USD. Cross-comparable exposure normalizer across NAICS for excess pricing. }
      required: [lineType, attachment, limit]

    EndorsementOp:
      oneOf:
        - { type: object, required: [op, tower_id, new_limit],      properties: { op: { const: change_limit },      tower_id: { type: string, format: uuid }, new_limit: { type: integer, format: int64 } } }
        - { type: object, required: [op, tower_id, new_attachment], properties: { op: { const: change_attachment }, tower_id: { type: string, format: uuid }, new_attachment: { type: integer, format: int64 } } }
        - { type: object, required: [op, tower_id, field, new_value], properties: { op: { const: change_premium }, tower_id: { type: string, format: uuid }, field: { type: string, enum: [premPremium, prodPremium] }, new_value: { type: number } } }
        - { type: object, required: [op, tower],     properties: { op: { const: add_tower },    tower: { $ref: '#/components/schemas/Tower' } } }
        - { type: object, required: [op, tower_id],  properties: { op: { const: remove_tower }, tower_id: { type: string, format: uuid } } }
        - { type: object, required: [op, address],   properties: { op: { const: change_address }, address: { type: object, properties: { line1: { type: string }, line2: { type: string }, city: { type: string }, state: { type: string }, zip: { type: string } } } } }
        - { type: object, required: [op, new_named_insured], properties: { op: { const: change_insured }, new_named_insured: { type: string }, new_naics_code: { type: string } } }

    VarianceCharged:
      type: object
      description: |
        Optional override of the server-rated premium. Internal-only —
        partner keys never carry the required scope, so attaching this
        from a partner API call returns 403.
      required: [amount, reason_cd]
      properties:
        amount:    { type: number, description: Dollar amount actually billed }
        reason_cd:
          type: string
          enum: [UNDERWRITER_DISCRETION, RENEWAL_CREDIT, RENEWAL_DEBIT, FLAT_CANCELLATION, AUDIT_WAIVED, AUDIT_JUDGMENT, OTHER_APPROVED]

    EndorsementCreate:
      type: object
      required: [policy_id, effective_date, changes]
      properties:
        policy_id:      { type: string, format: uuid }
        effective_date: { type: string, format: date }
        changes:        { type: array, minItems: 1, items: { $ref: '#/components/schemas/EndorsementOp' } }
        description:    { type: string }
        scenario_only:
          type: boolean
          default: false
          description: When true, draft is rated but bind returns `422 scenario-not-bindable`.

    EndorsementDraft:
      type: object
      properties:
        endorsement_id:        { type: string, format: uuid }
        status:                { type: string, enum: [draft, bound, expired, superseded, abandoned] }
        policy_id:             { type: string, format: uuid }
        effective_date:        { type: string, format: date }
        valid_until:           { type: string, format: date-time }
        system_premium:        { type: [number, "null"], description: Server-rated premium }
        charged_premium:       { type: [number, "null"], description: Equal to system_premium unless variance was applied at bind }
        variance_reason_cd:    { type: [string, "null"] }
        pro_rata_factor:       { type: [number, "null"] }
        pro_rated_delta:       { type: [number, "null"] }
        full_term_delta:       { type: [number, "null"] }
        new_written_premium:   { type: [number, "null"] }
        factor_version:        { type: object, additionalProperties: true }
        changes:               { type: array, items: { $ref: '#/components/schemas/EndorsementOp' } }
        description:           { type: [string, "null"] }
        warnings:              { type: array, items: { type: string } }
        bound_transaction_id:  { type: [string, "null"], format: uuid }
        created_at:            { type: string, format: date-time }
        bound_at:              { type: [string, "null"], format: date-time }
        scenario_only:         { type: boolean }

    # ---------- Cancellation ----------
    CancellationCreate:
      type: object
      required: [policy_id, cancellation_date, method, reason_code]
      properties:
        policy_id:         { type: string, format: uuid }
        cancellation_date: { type: string, format: date }
        method:            { type: string, enum: [pro_rata, short_rate], description: Flat cancellations are expressed as pro_rata + a variance code at bind time. }
        reason_code:       { type: string, description: "Free-form code from the partner's catalog (broker_request, non_payment, etc.)." }
        notice_date:       { type: string, format: date, description: Date partner notified the insured. Required for short_rate. }
        scenario_only:     { type: boolean, default: false, description: Rate but reject bind with 422 scenario-not-bindable. }
    CancellationDraft:
      type: object
      properties:
        cancellation_id:      { type: string, format: uuid }
        status:               { type: string, enum: [draft, bound, expired, superseded, abandoned] }
        policy_id:            { type: string, format: uuid }
        cancellation_date:    { type: string, format: date }
        method:               { type: string, enum: [pro_rata, short_rate] }
        reason_code:          { type: string }
        notice_date:          { type: [string, "null"], format: date }
        is_noc:               { type: boolean, description: Notice-of-cancellation flag. }
        return_premium:       { type: number }
        earned_premium:       { type: number }
        pro_rata_factor:      { type: number }
        pct_elapsed:          { type: number, description: Fraction of the policy term elapsed at cancellation_date. }
        short_rate_penalty:   { type: [number, "null"] }
        mep_floor:            { type: number, description: Minimum earned premium floor enforced by the policy. }
        mep_applied:          { type: boolean }
        days_remaining:       { type: integer }
        days_total:           { type: integer }
        new_written_premium:  { type: number }
        commission_clawback:  { type: number }
        factor_version:       { type: object, additionalProperties: true }
        valid_until:          { type: string, format: date-time }
        bound_transaction_id: { type: [string, "null"], format: uuid }
        bound_at:             { type: [string, "null"], format: date-time }
        variance_reason_cd:   { type: [string, "null"] }
        charged_premium:      { type: [number, "null"] }
        system_premium:       { type: [number, "null"] }
        scenario_only:        { type: boolean }

    # ---------- Extension ----------
    ExtensionCreate:
      type: object
      required: [policy_id, extended_expiration]
      properties:
        policy_id:           { type: string, format: uuid }
        extended_expiration: { type: string, format: date, description: New expiration date — must be after the policy's current expiration. }
        scenario_only:       { type: boolean, default: false }
    ExtensionDraft:
      type: object
      properties:
        extension_id:         { type: string, format: uuid }
        status:               { type: string, enum: [draft, bound, expired, superseded, abandoned] }
        policy_id:            { type: string, format: uuid }
        effective_date:       { type: string, format: date }
        original_expiration:  { type: string, format: date }
        new_expiration:       { type: string, format: date }
        extension_days:       { type: integer }
        daily_rate:           { type: number }
        additional_premium:   { type: number }
        new_written_premium:  { type: number }
        refreshed_mep_floor:  { type: number }
        commission:           { type: number }
        valid_until:          { type: string, format: date-time }
        bound_transaction_id: { type: [string, "null"], format: uuid }
        bound_at:             { type: [string, "null"], format: date-time }
        variance_reason_cd:   { type: [string, "null"] }
        charged_premium:      { type: [number, "null"] }
        system_premium:       { type: [number, "null"] }
        scenario_only:        { type: boolean }

    # ---------- Audit ----------
    AuditExposure:
      type: object
      description: One audited exposure unit (e.g. payroll class).
      properties:
        class_code:        { type: string }
        description:       { type: string }
        exposure_amount:   { type: number, description: "Audited basis (payroll, sales, units)." }
        rate:              { type: number }
        adjustment_amount: { type: number, description: Premium delta from estimated to audited. }
    AuditCreate:
      type: object
      required: [policy_id, effective_date, audit_period_start, audit_period_end]
      properties:
        policy_id:          { type: string, format: uuid }
        effective_date:     { type: string, format: date, description: Date the audit transaction applies to (typically term-end). }
        audit_period_start: { type: string, format: date }
        audit_period_end:   { type: string, format: date }
        exposures:          { type: array, items: { $ref: '#/components/schemas/AuditExposure' } }
        scenario_only:      { type: boolean, default: false }
    AuditDraft:
      type: object
      properties:
        audit_id:               { type: string, format: uuid }
        status:                 { type: string, enum: [draft, bound, expired, superseded, abandoned] }
        policy_id:              { type: string, format: uuid }
        effective_date:         { type: string, format: date }
        audit_period_start:     { type: string, format: date }
        audit_period_end:       { type: string, format: date }
        written_premium_before: { type: number }
        new_written_premium:    { type: number }
        adjustment:             { type: number, description: Premium delta from estimated to audited. }
        commission:             { type: number }
        exposures:              { type: array, items: { $ref: '#/components/schemas/AuditExposure' } }
        has_manual:             { type: boolean, description: True if any exposure was manually overridden. }
        valid_until:            { type: string, format: date-time }
        bound_transaction_id:   { type: [string, "null"], format: uuid }
        bound_at:               { type: [string, "null"], format: date-time }
        variance_reason_cd:     { type: [string, "null"] }
        charged_premium:        { type: [number, "null"] }
        system_premium:         { type: [number, "null"] }
        scenario_only:          { type: boolean }

    # ---------- Renewal ----------
    RenewalCreate:
      type: object
      required: [policy_id, new_effective_date]
      properties:
        policy_id:           { type: string, format: uuid, description: The expiring policy being renewed. Stored as prior_policy_id on the response. }
        new_effective_date:  { type: string, format: date }
        new_expiration_date: { type: string, format: date, description: Defaults to prior expiration + term length when omitted. }
        changes:             { type: array, items: { $ref: '#/components/schemas/EndorsementOp' }, description: "Optional in-renewal changes (limit bumps, coverage swaps)." }
        scenario_only:       { type: boolean, default: false }
    RenewalDraft:
      type: object
      properties:
        renewal_id:            { type: string, format: uuid }
        status:                { type: string, enum: [draft, bound, expired, superseded, abandoned] }
        prior_policy_id:       { type: string, format: uuid }
        new_effective_date:    { type: string, format: date }
        new_expiration_date:   { type: string, format: date }
        new_total_premium:     { type: number }
        prior_written_premium: { type: number }
        rate_change:           { type: number, description: "Fractional rate change vs the prior term (e.g. 0.05 = +5%)." }
        factor_version:       { type: object, additionalProperties: true }
        successor_towers:     { type: array, items: { $ref: '#/components/schemas/Tower' } }
        successor_account:    { type: object, additionalProperties: true }
        minimum_premium:      { type: number }
        changes:              { type: array, items: { $ref: '#/components/schemas/EndorsementOp' } }
        valid_until:          { type: string, format: date-time }
        bound_transaction_id: { type: [string, "null"], format: uuid }
        bound_at:             { type: [string, "null"], format: date-time }
        variance_reason_cd:   { type: [string, "null"] }
        charged_premium:      { type: [number, "null"] }
        system_premium:       { type: [number, "null"] }
        warnings:             { type: array, items: { type: string } }
        scenario_only:        { type: boolean }

    # ---------- Rewrite ----------
    RewriteCreate:
      type: object
      required: [policy_id, rewrite_date, new_expiration_date]
      properties:
        policy_id:            { type: string, format: uuid, description: The policy being rewritten. }
        rewrite_date:         { type: string, format: date }
        new_expiration_date:  { type: string, format: date }
        reason:               { type: string, default: broker_request, description: 'broker_request, named_insured_change, midterm_correction, etc.' }
        new_named_insured:    { type: string, description: Optional rename of the insured entity. }
        new_commission_rate:  { type: number }
        changes:              { type: array, items: { $ref: '#/components/schemas/EndorsementOp' } }
        scenario_only:        { type: boolean, default: false }
    RewriteDraft:
      type: object
      properties:
        rewrite_id:           { type: string, format: uuid }
        status:               { type: string, enum: [draft, bound, expired, superseded, abandoned] }
        prior_policy_id:      { type: string, format: uuid }
        rewrite_date:         { type: string, format: date }
        new_expiration_date:  { type: string, format: date }
        new_total_premium:     { type: number }
        prior_written_premium: { type: number }
        factor_version:        { type: object, additionalProperties: true }
        new_named_insured:     { type: [string, "null"] }
        new_commission_rate:  { type: [number, "null"] }
        reason:               { type: string }
        successor_towers:     { type: array, items: { $ref: '#/components/schemas/Tower' } }
        successor_account:    { type: object, additionalProperties: true }
        minimum_premium:      { type: number }
        changes:              { type: array, items: { $ref: '#/components/schemas/EndorsementOp' } }
        valid_until:          { type: string, format: date-time }
        bound_transaction_id: { type: [string, "null"], format: uuid }
        bound_at:             { type: [string, "null"], format: date-time }
        variance_reason_cd:   { type: [string, "null"] }
        charged_premium:      { type: [number, "null"] }
        system_premium:       { type: [number, "null"] }
        warnings:             { type: array, items: { type: string } }
        scenario_only:        { type: boolean }

    # ---------- OOS rebase ----------
    OosRebaseCreate:
      type: object
      required: [policy_id, oos_effective_date]
      properties:
        policy_id:          { type: string, format: uuid }
        oos_effective_date: { type: string, format: date, description: "Effective date of the back-dated change. Must be before the latest applied transaction." }
        oos_changes:        { type: array, items: { $ref: '#/components/schemas/EndorsementOp' }, description: "Structured changes — server re-rates the OOS slice + every downstream replacement against the post-OOS basis." }
        oos_premium_change: { type: number, description: "Legacy dollar-amount path. Use oos_changes for server-rated, drift-detectable rebases." }
        description:        { type: string }
        scenario_only:      { type: boolean, default: false }
    OosRebaseRow:
      type: object
      properties:
        kind:                { type: string, enum: [oos, oos_offset, oos_replacement] }
        sequence_number:     { type: integer }
        transaction_type:    { type: string }
        effective_date:      { type: string, format: date }
        premium_change:      { type: number }
        new_total_premium:   { type: number }
        rebase_of_txn_id:    { type: [string, "null"], format: uuid }
        endorsement_details: { type: object, additionalProperties: true }
        meta:                { type: object, additionalProperties: true }
        description:         { type: string }
    OosRebaseDraft:
      type: object
      properties:
        rebase_id:             { type: string, format: uuid }
        status:                { type: string, enum: [draft, bound, expired, superseded, abandoned] }
        policy_id:             { type: string, format: uuid }
        oos_effective_date:    { type: string, format: date }
        oos_premium_change:    { type: number }
        oos_full_term_delta:   { type: number }
        insertion_sequence:    { type: integer, description: Sequence position the OOS row will be inserted at. }
        rows:                  { type: array, items: { $ref: '#/components/schemas/OosRebaseRow' }, description: Full plan — OOS slice + offsets reversing each downstream + replacements re-applying them on the new basis. }
        net_premium_impact:    { type: number }
        downstream_count:      { type: integer }
        final_written_premium: { type: number }
        rerated_replacements:  { type: boolean }
        strict_rerate:         { type: boolean }
        factor_version:        { type: object, additionalProperties: true }
        warnings:              { type: array, items: { type: string }, description: 'oos.replay_failed: / oos.replay_warning: prefixed strings tagging the offending downstream.' }
        valid_until:           { type: string, format: date-time }
        bound_transaction_id:  { type: [string, "null"], format: uuid }
        bound_at:              { type: [string, "null"], format: date-time }
        scenario_only:         { type: boolean }

    # ---------- Bound transaction envelope ----------
    BoundTransactionId:
      type: object
      description: Returned in the body of every successful bind alongside the resource Draft fields.
      properties:
        transaction_id: { type: string, format: uuid, description: The id of the committed transaction this bind produced. }

    # ---------- Read endpoint row schemas ----------
    InsuredAccount:
      type: object
      description: |
        Canonical insured account. Surrogate UUID identity; FEIN is nullable
        (DBA-only / sole-proprietor accounts). Account-level data persists
        across all submissions, quotes, and policies for the insured.
        Distinct from the rate-quote `Account` payload, which is per-quote
        broker-supplied data (named insured, NAICS, exposures) — `InsuredAccount`
        is the carrier-side canonical entity.
      properties:
        id:                       { type: string, format: uuid }
        named_insured:            { type: string }
        fein:                     { type: [string, "null"], description: 9-digit FEIN (digits or hyphenated) }
        dba:                      { type: [string, "null"] }
        naics_code:               { type: [string, "null"] }
        ops_description:          { type: [string, "null"] }
        primary_address_line1:    { type: [string, "null"] }
        primary_address_city:     { type: [string, "null"] }
        primary_address_state:    { type: [string, "null"], description: 2-letter US state }
        primary_address_zip:      { type: [string, "null"] }
        parent_account_id:        { type: [string, "null"], format: uuid, description: Parent account in a corporate-group hierarchy }
        superseded_by_account_id: { type: [string, "null"], format: uuid, description: New account that replaced this one (M&A / FEIN change) }
        created_at:               { type: string, format: date-time }
        updated_at:               { type: string, format: date-time }
    Submission:
      type: object
      properties:
        id:                { type: string, format: uuid }
        submission_number: { type: string }
        status:            { type: string }
        named_insured:     { type: string }
        naics_code:        { type: string }
        address_state:     { type: string }
        agency_group_id:   { type: string, format: uuid }
        created_at:        { type: string, format: date-time }
    Quote:
      type: object
      properties:
        id:                { type: string, format: uuid }
        quote_number:      { type: string }
        submission_id:     { type: string, format: uuid }
        status:            { type: string }
        total_premium:     { type: number }
        technical_premium: { type: number }
        valid_until:       { type: string, format: date-time }
        created_at:        { type: string, format: date-time }
    Claim:
      type: object
      properties:
        id:               { type: string, format: uuid }
        claim_number:     { type: string }
        policy_id:        { type: string, format: uuid }
        loss_date:        { type: string, format: date }
        report_date:      { type: string, format: date }
        status:           { type: string }
        total_incurred:   { type: number }
        paid_to_date:     { type: number }
        reserve:          { type: number }
        coverage_line:    { type: string, enum: [GL, AUTO, LIQUOR] }
        description:      { type: [string, "null"] }
    Document:
      type: object
      description: Document metadata only (filename / form catalog mapping). File bytes are not delivered via this endpoint.
      properties:
        id:               { type: string, format: uuid }
        policy_id:        { type: string, format: uuid }
        transaction_id:   { type: [string, "null"], format: uuid }
        form_code:        { type: string }
        form_title:       { type: string }
        document_type:    { type: string, enum: [policy_package, endorsement, cancellation, audit, claim_acknowledgment, certificate] }
        generated_at:     { type: [string, "null"], format: date-time }
    PolicyTransaction:
      type: object
      properties:
        id:                  { type: string, format: uuid }
        policy_id:           { type: string, format: uuid }
        sequence_number:     { type: integer }
        transaction_type:    { type: string, enum: [submission, endorsement, cancellation, reinstatement, extension, audit, renewal, rewrite, oos_rebase] }
        effective_date:      { type: string, format: date }
        premium_change:      { type: number }
        new_total_premium:   { type: number }
        endorsement_number:  { type: [string, "null"] }
        status:              { type: string, enum: [pending, applied, voided, superseded] }
        applied_at:          { type: [string, "null"], format: date-time }
        endorsement_details: { type: object, additionalProperties: true }
        meta:                { type: object, additionalProperties: true }

    Policy:
      type: object
      properties:
        id:                       { type: string, format: uuid }
        policy_number:            { type: string }
        status:                   { type: string }
        effective_date:           { type: string, format: date }
        expiration_date:          { type: string, format: date }
        named_insured:            { type: string }
        current_written_premium:  { type: number }
        agency_group_id:          { type: string, format: uuid }

    RateQuoteRequest:
      type: object
      required: [account, towers]
      properties:
        account:
          type: object
          required: [naicsClass]
          properties:
            naicsClass:    { type: string }
            effectiveDate: { type: string, format: date, description: Selects the rating factor version. Defaults to today; 422 if no factor version covers it. }
        towers:
          type: array
          minItems: 1
          items: { $ref: '#/components/schemas/Tower' }
        modifiers:
          type: object
          description: All multipliers default to 1.0. Technical premium is always rated with modifiers at 1.0.
          properties:
            subjectivityMultiplier: { type: number, default: 1.0 }
            experienceMod:          { type: number, default: 1.0 }
            coverageMod:            { type: number, default: 1.0 }
            policyHazardOverride:   { type: number, nullable: true, description: Hazard-grade override; null applies the NAICS blend. }
        marketFactor: { type: number, default: 1.0 }

    RateQuoteResponse:
      type: object
      properties:
        success:           { type: boolean }
        factorVersion:
          type: object
          description: The rating factor version that priced this request.
          properties:
            id:    { type: string }
            label: { type: string }
        effectiveDate:     { type: string, format: date }
        technicalPremium:  { type: number, description: Integer USD. All modifiers at 1.0. }
        policyPremium:     { type: number, description: Integer USD. After all modifiers. }
        ratePerMil:        { type: number, description: USD per $1M of total limit, 2 decimals. 0 if no limit supplied. }
        modifierBreakdown:
          type: object
          properties:
            market:       { type: number }
            subjectivity: { type: number }
            experience:   { type: number }
            coverage:     { type: number }
            totalMod:     { type: number }
        minimumPremium:
          type: object
          properties:
            total:     { type: number }
            isBinding: { type: boolean }
        hazard:
          type: object
          nullable: true
          description: Hazard-grade detail behind the rating pass.
          properties:
            applied:  { type: number }
            blended:  { type: number }
            max:      { type: number }
            override: { type: boolean }
        towerResults:
          type: array
          items:
            type: object
            properties:
              id:               { type: string }
              lineType:         { type: string, enum: [GL, AUTO, LIQUOR] }
              attachment:       { type: integer, format: int64 }
              limit:            { type: integer, format: int64 }
              technicalPremium: { type: number }
              policyPremium:    { type: number }
              blendedHazard:    { type: number }

