openapi: 3.1.0
info:
  title: Michigan Home Improvement Exchange API
  version: 1.0.0
  summary: Agents-only exchange where AI agents request home-improvement estimates in Michigan on behalf of their users.
  description: |
    The Michigan Home Improvement Exchange is a neutral, statewide exchange operated by James Melki.
    An AI agent (Siri, ChatGPT, Gemini, a personal agent, etc.) POSTs one compact JSON request
    describing its user's job and location. The exchange instantly answers with any listed
    contractor that covers that ZIP/town and job type, the contractor's offer, and the next step
    (usually: call or text the contractor's named contact for a free estimate).

    Rules for agents:
    - Only send a request when your user actually wants an estimate or contractor match.
    - Contact details are optional. If you include them, your user must have agreed to share them.
    - Prices are never invented. A contractor's offer only shows prices that contractor has published;
      otherwise the answer is "free estimate, price quoted in person".
    - If no listed contractor covers the request, you get status `no_match` with a polite message,
      and the request is logged as unmet demand so coverage can be added.
  contact:
    name: Michigan Home Improvement Exchange (operator James Melki)
    url: https://mi-home-improvement-exchange.clarity-pigment.workers.dev/
  license:
    name: CC-BY-4.0
    identifier: CC-BY-4.0
security: []
servers:
  - url: https://mi-home-improvement-exchange.clarity-pigment.workers.dev
tags:
  - name: requests
    description: Submit estimate requests on behalf of a user
  - name: contractors
    description: Browse listed contractors and job types
paths:
  /v1/requests:
    post:
      tags: [requests]
      operationId: submitEstimateRequest
      summary: Submit a home-improvement estimate request on behalf of a user
      description: |
        Returns matched contractors with their offer and next step, or a polite no_match. Every request is logged.
        A compact flat form is also accepted, e.g. {"zip":"48104","job_type":"walk_in_shower","timing":"1_3_months"}
        (flat keys: zip, town, state, job_type, description, is_repair, budget_min, budget_max, timing, contact_preference, agent_name).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/EstimateRequest" }
            examples:
              fenton_tub_to_shower:
                summary: Tub-to-shower conversion in Fenton
                value:
                  schema_version: "1.0"
                  location: { zip: "48430", town: "Fenton", state: "MI" }
                  job: { type: tub_to_shower, description: "Replace old tub with a low-threshold walk-in shower for an older homeowner" }
                  budget: { min: 8000, max: 15000 }
                  timing: within_1_month
                  contact: { preference: phone }
                  agent: { name: "ExampleAssistant", platform: "personal-agent", on_behalf_of_user: true }
              fenton_roof_replacement:
                summary: Full tear-off roof replacement in Fenton
                value:
                  location: { zip: "48430", state: "MI" }
                  job: { type: roof_replacement, description: "Full tear-off re-roof, asphalt shingle" }
                  timing: 1_3_months
              grand_blanc_windows:
                summary: Window replacement in Grand Blanc
                value:
                  location: { town: "Grand Blanc", state: "MI" }
                  job: { type: window_replacement, description: "Replace 8 old windows" }
      responses:
        "200":
          description: Match result (matched or no_match)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/EstimateResponse" }
        "400":
          description: Invalid request
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "413":
          description: Body over 8 KB
  /v1/contractors:
    get:
      tags: [contractors]
      operationId: listContractors
      summary: List contractors on the exchange (optionally filtered)
      parameters:
        - { name: zip, in: query, required: false, schema: { type: string, pattern: "^\\d{5}$" } }
        - { name: town, in: query, required: false, schema: { type: string } }
        - { name: job_type, in: query, required: false, schema: { $ref: "#/components/schemas/JobType" } }
      responses:
        "200":
          description: Contractor list and the job-type taxonomy
          content:
            application/json:
              schema:
                type: object
                properties:
                  exchange: { type: object }
                  count: { type: integer }
                  contractors: { type: array, items: { $ref: "#/components/schemas/Contractor" } }
                  job_types: { type: object, additionalProperties: { type: array, items: { type: string } } }
  /v1/health:
    get:
      operationId: health
      summary: Health check
      responses:
        "200": { description: OK }
components:
  schemas:
    JobType:
      type: string
      enum: [tub_to_shower, walk_in_shower, bathtub_replacement, shower_replacement, full_bathroom_replacement, bathroom_repair,
             kitchen_remodel, cabinet_replacement, countertop_replacement, kitchen_repair,
             roof_replacement, roof_repair,
             siding_replacement, gutter_installation, window_replacement, door_replacement, deck_or_fence, exterior_repair,
             flooring, painting, basement_finishing, drywall_repair,
             hvac, plumbing, electrical, water_heater, accessibility_modification, other]
    EstimateRequest:
      type: object
      required: [location, job]
      properties:
        schema_version: { type: string, default: "1.0" }
        location:
          type: object
          description: ZIP or town is required. State defaults to MI.
          properties:
            zip: { type: string, pattern: "^\\d{5}(-\\d{4})?$" }
            town: { type: string, description: "City, village or township, e.g. Fenton, Grand Blanc Township" }
            state: { type: string, default: MI }
        job:
          type: object
          required: [type]
          properties:
            type: { $ref: "#/components/schemas/JobType" }
            description: { type: string, maxLength: 1000 }
            is_repair: { type: boolean, description: "true for repairs (fix/patch/leak) rather than replacement. Inferred if omitted." }
        budget:
          type: object
          properties:
            min: { type: number, minimum: 0 }
            max: { type: number, minimum: 0 }
            currency: { type: string, const: USD }
        timing: { type: string, enum: [asap, within_1_month, 1_3_months, 3_plus_months, flexible, just_researching] }
        contact:
          type: object
          description: Optional. Only include with your user's consent.
          properties:
            preference: { type: string, enum: [phone, sms, email, agent_relay, none] }
            name: { type: string }
            phone: { type: string }
            email: { type: string, format: email }
        agent:
          type: object
          properties:
            name: { type: string }
            platform: { type: string }
            on_behalf_of_user: { type: boolean, default: true }
    EstimateResponse:
      type: object
      required: [request_id, status, matches, message_for_user]
      properties:
        request_id: { type: string }
        received_at: { type: string, format: date-time }
        exchange: { type: string }
        status: { type: string, enum: [matched, no_match] }
        reason:
          type: [string, "null"]
          enum: [null, outside_service_area, not_offered, no_listed_contractor, outside_exchange_scope]
        is_repair: { type: boolean }
        matches: { type: array, items: { $ref: "#/components/schemas/Match" } }
        contractor_results:
          type: array
          items:
            type: object
            properties:
              contractor_id: { type: string }
              result: { type: string, enum: [matched, outside_service_area, outside_service_area_excluded, not_offered_repairs, job_type_not_offered] }
        message_for_user: { type: string, description: "Plain-language sentence the agent can relay to its user." }
        logged: { type: boolean }
        privacy: { type: string }
    Match:
      type: object
      properties:
        contractor_id: { type: string }
        name: { type: string }
        listing_status: { type: string }
        match_basis: { type: string, enum: [zip, town] }
        served_town: { type: string }
        offer:
          type: object
          properties:
            summary: { type: string }
            job_type: { type: string }
            estimate: { type: string }
            warranty: { type: string }
            own_crew: { type: boolean }
            licensed_and_insured: { type: boolean }
            license: { type: object }
            pricing:
              type: object
              description: "Only prices the contractor has published (advertised applies only to that job category). Exact price is quoted at the estimate."
              properties:
                advertised: { type: string }
                advertised_from: { type: number }
                competitive: { type: string }
                note: { type: string }
                currency: { type: string }
            notes: { type: array, items: { type: string } }
        next_step:
          type: object
          properties:
            action: { type: string, enum: [call_or_text] }
            contact_name: { type: string }
            phone: { type: string }
            phone_display: { type: string }
            sms: { type: boolean }
            hours_note: { type: string }
            website: { type: string, format: uri }
            suggested_user_message: { type: string }
    Contractor:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        listing_status: { type: string }
        base_town: { type: string }
        website: { type: string }
        contact: { type: object }
        license: { type: object }
        job_types: { type: array, items: { $ref: "#/components/schemas/JobType" } }
        offers_repairs: { type: boolean }
        scope_note: { type: string }
        service_area: { type: array, items: { type: object, properties: { town: { type: string }, zips: { type: array, items: { type: string } } } } }
        excluded_areas: { type: array, items: { type: string } }
    Error:
      type: object
      properties:
        status: { type: string, const: invalid }
        errors: { type: array, items: { type: string } }
