openapi: 3.1.0
info:
  title: Open Hood public API
  version: 1.1.0
  summary: Consumer advocate — VIN, quotes, shops, recalls, integrations. Native fetch only.
  description: >
    Public routes that exist on this origin. POST /api/agent runs bay tools then writes a
    Beginner or Expert brief. GET /api/integrations/status is the honesty matrix: public
    probes, weekend keys configured:false when missing (probed:skip when on), Stripe test
    packet Checkout, live packet only with an existing Price ID, escrow refused.
    GET /agent/api renders this spec. GET /api/openapi is the live contract.
    Not a shop booking API. Not TecDoc. Not a Carfax feed.
servers:
  - url: /
    description: Same origin as the bay
tags:
  - name: Advocate
    description: Advocate desk + bay tools
  - name: Integrations
    description: Patch bay, probes, honest no-op adapters
  - name: Vehicle
    description: VIN, fluids, catalog menus
  - name: Shops
    description: Directory / maps. Overpass is a separate desk.
  - name: Ticket
    description: Quote, estimate, history, trust, contact
  - name: Spec
    description: This OpenAPI document
paths:
  /api/agent:
    get:
      tags: [Advocate]
      operationId: getAgentStatus
      summary: Desk status and tool list
      responses:
        "200":
          description: Status
    post:
      tags: [Advocate]
      operationId: postAgent
      summary: Ask the advocate
      description: Native fetch. JSON or SSE when Accept includes text/event-stream.
      x-openhood-fetch: |
        const r = await fetch('/api/agent', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: [{ role: 'user', content: 'P0420 on a 2003 Honda Accord' }], readingLevel: 'beginner' }) });
        const brief = await r.json();
      responses:
        "200":
          description: JSON brief or SSE
        "400":
          description: Missing messages
  /api/openapi:
    get:
      tags: [Spec]
      operationId: getOpenApi
      summary: OpenAPI 3.1 document
      parameters:
        - name: format
          in: query
          schema:
            type: string
            enum: [yaml, json]
      responses:
        "200":
          description: OpenAPI 3.1
  /api/integrations/status:
    get:
      tags: [Integrations]
      operationId: getIntegrationsStatus
      summary: Honesty matrix + public probes
      description: >
        Every jack. Live probes for NHTSA vPIC, Nominatim, FuelEconomy.gov.
        recallsByVin probe is 403. Paid keys stay configured:false unless wired and on
        (then probed:skip). Chrome / MOTOR / TecDoc stay false. Stripe test → configured
        true, product none; live → liveKeyDetected, product none, escrow refused.
        ?probe=0 skips upstream probes.
      x-openhood-fetch: |
        const r = await fetch('/api/integrations/status');
        const j = await r.json();
      responses:
        "200":
          description: Matrix, keys, probes, stripe, licensed empty catalog
  /api/integrations/nhtsa:
    get:
      tags: [Integrations]
      summary: vPIC / SaferCar / NCAP / complaints
      description: kind=vin|vin-campaign|recallsByVin|recalls|complaints|ratings. recallsByVin reports the 403.
      x-openhood-fetch: |
        const r = await fetch('/api/integrations/nhtsa?kind=vin&vin=1HGCM82633A004352');
        const { specs } = await r.json();
      responses:
        "200":
          description: Decode or campaigns
  /api/integrations/mpg:
    get:
      tags: [Integrations]
      summary: EPA FuelEconomy.gov MPG
      x-openhood-fetch: |
        const r = await fetch('/api/integrations/mpg?year=2003&make=Honda&model=Accord');
        const { rows } = await r.json();
      responses:
        "200":
          description: Official MPG rows
  /api/integrations/geocode:
    get:
      tags: [Integrations]
      summary: Nominatim pin
      x-openhood-fetch: |
        const r = await fetch('/api/integrations/geocode?q=97214');
        const pin = await r.json();
      responses:
        "200":
          description: lat/lon
  /api/integrations/youtube:
    get:
      tags: [Integrations]
      summary: YouTube how-to
      description: No scrape. Key off → videos:[], searchUrl still works.
      x-openhood-fetch: |
        const r = await fetch('/api/integrations/youtube?q=cabin+filter+how+to');
        const { youtube, searchUrl } = await r.json();
      responses:
        "200":
          description: Clips or empty + search URL
  /api/integrations/links:
    get:
      tags: [Integrations]
      summary: Computed outbound hrefs
      responses:
        "200":
          description: Open / Connect / Request access URLs
  /api/integrations/places:
    get:
      tags: [Integrations]
      summary: Google Places adapter
      description: Missing key → configured:false, results:[]. No invented shops.
      responses:
        "200":
          description: Places or empty
  /api/integrations/yelp:
    get:
      tags: [Integrations]
      summary: Yelp Fusion adapter
      description: Missing key → configured:false, results:[].
      responses:
        "200":
          description: Fusion or empty
  /api/integrations/plate:
    get:
      tags: [Integrations]
      summary: Plate-to-VIN status
      responses:
        "200":
          description: Dark or live
    post:
      tags: [Integrations]
      summary: Plate-to-VIN (no-op without keys)
      description: No key → vin:null. We do not invent a VIN, shop, or price.
      x-openhood-fetch: |
        const r = await fetch('/api/integrations/plate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ plate: 'ABC123', state: 'OR' }) });
        const j = await r.json();
      responses:
        "200":
          description: Decode or honest dark body
  /api/integrations/openai:
    get:
      tags: [Integrations]
      summary: Photo reading status
      description: GET only. Does not call OpenAI.
      responses:
        "200":
          description: configured true|false
  /api/integrations/chrome:
    get:
      tags: [Integrations]
      summary: Chrome Data stub
      description: Always connected:false. ROADMAP after contract.
      responses:
        "200":
          description: Empty styles
  /api/integrations/motor:
    get:
      tags: [Integrations]
      summary: MOTOR / Identifix stub
      description: Always configured:false. No invented hours. ROADMAP.
      responses:
        "200":
          description: Empty hours
  /api/integrations/stripe:
    get:
      tags: [Integrations]
      summary: Stripe test vs live vs none
      description: sk_test_ → configured true, product packet, Checkout. sk_live_ → packet fee only with STRIPE_PACKET_PRICE_ID. Escrow refused.
      responses:
        "200":
          description: Packet honesty
  /api/integrations/catalog:
    get:
      tags: [Integrations]
      summary: Licensed catalog empty bay
      description: skus:[], hours:[], tecdoc/motor/partstech/chrome false. Contract names + ROADMAP. No fake SKUs.
      responses:
        "200":
          description: Empty catalog
  /api/integrations/partstech:
    get:
      tags: [Integrations]
      summary: PartsTech empty adapter
      description: Always connected:false, skus:[]. PartsTech shop parts book. ROADMAP.
      responses:
        "200":
          description: Empty SKUs
  /api/integrations/vinaudit:
    get:
      tags: [Integrations]
      summary: VinAudit weekend key
      description: configured true only with VINAUDIT_API_KEY. Never a dummy Carfax.
      responses:
        "200":
          description: Status
  /api/stripe/packet:
    get:
      tags: [Integrations]
      summary: Packet fee status
      description: Optional print packet / hold this bay. Test vs live vs none. Escrow refused.
      responses:
        "200":
          description: Status or session
    post:
      tags: [Integrations]
      summary: Open packet Checkout
      description: sk_test_ or sk_live_ plus existing price_. Founder can complete a test payment. Not escrow.
      responses:
        "200":
          description: Checkout URL
        "403":
          description: Live without product
        "404":
          description: No key
  /api/vin:
    post:
      tags: [Vehicle]
      summary: Decode a VIN
      x-openhood-fetch: |
        const r = await fetch('/api/vin', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ vin: '1HGCM82633A004352' }) });
        const { specs } = await r.json();
      responses:
        "200":
          description: specs, fluids, recalls
  /api/identify:
    post:
      tags: [Vehicle]
      summary: Identify from VIN or year/make/model
      responses:
        "200":
          description: Identity packet
  /api/fluids:
    get:
      tags: [Vehicle]
      summary: Fluids / PSI card
      x-openhood-fetch: |
        const r = await fetch('/api/fluids?year=2003&make=Honda&model=Accord');
        const { fluids } = await r.json();
      responses:
        "200":
          description: Fluids card
  /api/catalog:
    get:
      tags: [Vehicle]
      summary: Year / make / model menus
      responses:
        "200":
          description: Makes or models
  /api/quote:
    post:
      tags: [Ticket]
      summary: Mark up a repair order
      responses:
        "200":
          description: Marked ticket
  /api/estimate:
    get:
      tags: [Ticket]
      summary: ZIP labor band
      description: Heuristic hours. Not Motor. Not Mitchell.
      responses:
        "200":
          description: Estimate
  /api/directory/search:
    get:
      tags: [Shops]
      summary: Overpass rooftops
      description: Owned by the directory desk. Documented here only.
      responses:
        "200":
          description: Places
  /api/directory/places:
    get:
      tags: [Shops]
      summary: Places / Yelp merge
      description: Missing keys → connected:false, results:[].
      responses:
        "200":
          description: Paid rooftops or empty
  /api/history:
    get:
      tags: [Ticket]
      summary: History dossier
      responses:
        "200":
          description: Identity + campaigns + link-outs
  /api/history/title:
    get:
      tags: [Ticket]
      summary: Paid title snapshot status
      description: VinAudit or CarsXE history only if that env key is on. Off → empty bay. Not a Carfax file.
      responses:
        "200":
          description: Status or snapshot
    post:
      tags: [Ticket]
      summary: Paid title snapshot
      description: JSON { vin }. No key → empty. We do not invent wrecks.
      responses:
        "200":
          description: Snapshot or empty bay
  /api/history/plate:
    get:
      tags: [Ticket]
      summary: History plate status
      responses:
        "200":
          description: Dark or live
    post:
      tags: [Ticket]
      summary: History plate decode
      responses:
        "200":
          description: Decode or note
  /api/contact:
    get:
      tags: [Ticket]
      summary: Contact mailer status
      responses:
        "200":
          description: delivery email or local
    post:
      tags: [Ticket]
      summary: Submit a contact ticket
      responses:
        "200":
          description: Filed
  /api/trust:
    get:
      tags: [Ticket]
      summary: Trust desk status
      responses:
        "200":
          description: Status
  /api/trust/hold:
    get:
      tags: [Ticket]
      summary: Hold status
      description: sk_live_ refused. Not escrow.
      responses:
        "200":
          description: Hold mode
  /api/ocr:
    post:
      tags: [Vehicle]
      summary: Photo OCR
      responses:
        "200":
          description: Read text
  /api/symptoms:
    get:
      tags: [Advocate]
      summary: Symptom map
      responses:
        "200":
          description: Shop questions
  /api/guides:
    get:
      tags: [Advocate]
      summary: Guide glossary
      responses:
        "200":
          description: Jobs
  /api/finder:
    get:
      tags: [Advocate]
      summary: Parts finder
      description: Storefront search URLs. Not live stock. Not TecDoc.
      responses:
        "200":
          description: Aisles
  /api/jobs/dtc:
    get:
      tags: [Advocate]
      summary: DTC book
      responses:
        "200":
          description: Code card
  /agent:
    get:
      tags: [Advocate]
      summary: Advocate desk
      responses:
        "200":
          description: HTML
  /agent/api:
    get:
      tags: [Spec]
      summary: Readable OpenAPI view
      responses:
        "200":
          description: HTML docs
  /integrations:
    get:
      tags: [Integrations]
      summary: Patch bay
      responses:
        "200":
          description: HTML
x-openhood-tools:
  decode_vin: NHTSA vPIC. 17-character VIN.
  lookup_dtc: Local DTC book plus generic SAE layout.
  analyze_quote_text: Local quote book. Flags flushes and padded lines.
  diagnose_symptoms: Noise plus when maps to shop questions.
  get_fluids_for_vehicle: Catalog or heuristic fluids card.
  search_guides: Owner guide glossary.
  search_directory: ZIP plus type to OSM rooftops. No booking.
  get_recalls: NHTSA year/make/model campaigns. Not VIN open/closed.
  get_epa_mpg: FuelEconomy.gov official MPG.
  search_playbooks: Named owner playbooks. Not a stolen TSB.
  lookup_ro_term: Invoice slang. Not legal advice.
  search_parts: Outbound catalog search URLs. Not TecDoc. Not live stock.
