openapi: 3.1.0
info:
  title: GroundQuote Installer API
  version: "1.0"
  description: |
    Create and configure contractor accounts on their behalf, then install the GroundQuote quoting widget.
    Free to install; the contractor pays only for leads they choose to unlock ($5 browsing, $15 high-intent)
    or an optional $150/month unlimited plan. Relay `terms_summary` to the contractor before creating an account.
    An installer key can create organizations and configure only the ones it created. It can never unlock leads,
    read homeowner contact details, or change billing.
  contact:
    email: info@kilvingtondigitalmarketing.com
servers:
  - url: https://www.groundquote.com/api/v1
security:
  - installerKey: []
components:
  securitySchemes:
    installerKey:
      type: http
      scheme: bearer
      description: "Installer key, format gq_inst_<40 hex>. Request one at https://www.groundquote.com/developers."
  schemas:
    Error:
      type: object
      properties:
        error: { type: string }
        fields:
          type: object
          additionalProperties: { type: string }
          description: Field path -> message, present on 422.
    CreateOrg:
      type: object
      required: [company_name, owner_email, owner_name, phone]
      properties:
        company_name: { type: string, minLength: 2, maxLength: 100 }
        owner_email: { type: string, format: email, description: The contractor who will own the account. }
        owner_name: { type: string }
        phone: { type: string }
        website_url: { type: string, format: uri }
        city: { type: string }
        state: { type: string, description: Two-letter US state code. }
        terms_relayed: { type: boolean, description: Confirm you told the contractor the pricing terms. }
    Branding:
      type: object
      properties:
        logo_url: { type: [string, "null"] }
        primary_color: { type: string }
        secondary_color: { type: string }
        accent_color: { type: string }
        font: { type: [string, "null"] }
    HostedPage:
      type: object
      properties:
        enabled: { type: boolean }
        headline: { type: [string, "null"] }
        subheading: { type: [string, "null"] }
        services: { type: array, items: { type: string } }
        phone: { type: [string, "null"] }
        email: { type: [string, "null"] }
        city: { type: [string, "null"] }
        state: { type: [string, "null"] }
        cover_url: { type: [string, "null"] }
    Pricing:
      type: object
      required: [fences, gates]
      description: |
        Same shape the dashboard stores. Material and style keys are the contractor's own names.
        Height keys look like "6 ft"; values are dollars per linear foot. Keys starting with "_" are config:
        _fenceRemoval {rate, enabled}, _leadConfig {capturePhone}, _modules ["fence"|"deck"|"patio"], _decks, _patios.
      properties:
        fences:
          type: object
          additionalProperties: true
          example:
            Wood: { Privacy: { "4 ft": 28, "6 ft": 36, "8 ft": 48 }, Picket: { "4 ft": 24, "6 ft": 30 } }
            Vinyl: { Privacy: { "6 ft": 55 } }
            _fenceRemoval: { rate: 4, enabled: true }
            _leadConfig: { capturePhone: true }
            _modules: [fence]
        gates:
          type: object
          required: [single, double]
          properties:
            single: { type: number, description: Flat surcharge per single gate, USD. }
            double: { type: number, description: Flat surcharge per double gate, USD. }
    Org:
      type: object
      properties:
        org_id: { type: string }
        slug: { type: string }
        company_name: { type: string }
        website_url: { type: [string, "null"] }
        owner_email: { type: string }
        status: { type: string, enum: [active, suspended] }
        plan: { type: string, enum: [pay_per_lead, unlimited] }
        terms_accepted: { type: boolean }
        branding: { $ref: "#/components/schemas/Branding" }
        install:
          type: object
          properties:
            embed_snippet: { type: string, description: The iframe HTML to paste. }
            embed_url: { type: string }
            hosted_page_enabled: { type: boolean }
            hosted_page_url: { type: string }
        hosted_page: { $ref: "#/components/schemas/HostedPage" }
        terms_summary: { type: string, description: Relay this to the contractor verbatim. }
        setup_link_pending: { type: boolean, description: True while the contractor has an unused set-password link. }
        created_at: { type: string, format: date-time }
paths:
  /terms:
    get:
      summary: Pricing disclosure (no key required)
      security: []
      responses:
        "200":
          description: terms_summary, prices, table, guarantees
  /orgs:
    post:
      summary: Create a contractor organization
      description: |
        Creates the org on the pay-per-lead plan (no card, nothing charged), seeds placeholder pricing, and emails
        the contractor the pricing terms with a 72-hour link to set their own password. Max 50 per key per day.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateOrg" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Org"
                  - type: object
                    properties:
                      setup_email_sent_to: { type: [string, "null"] }
                      next_steps: { type: array, items: { type: string } }
        "401": { description: Missing or invalid key }
        "409": { description: owner_email already registered }
        "422": { description: Validation failed, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "429": { description: Daily creation limit reached }
  /orgs/{org_id}:
    parameters:
      - { name: org_id, in: path, required: true, schema: { type: string } }
    get:
      summary: Status, branding and install details
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Org" } } } }
        "404": { description: Not found or not created by this key }
  /orgs/{org_id}/pricing:
    parameters:
      - { name: org_id, in: path, required: true, schema: { type: string } }
    get:
      summary: Current fence catalog
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Pricing" } } } }
    put:
      summary: Replace the fence catalog
      description: Materials and styles replace what is there; config keys not sent are kept. Never guess rates.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Pricing" }
      responses:
        "200": { description: Updated }
        "422": { description: Validation failed, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
  /orgs/{org_id}/branding:
    parameters:
      - { name: org_id, in: path, required: true, schema: { type: string } }
    put:
      summary: Set logo, colors and font
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                logo_url: { type: string, format: uri }
                primary_color: { type: string, example: "#1d4ed8" }
                secondary_color: { type: string }
                accent_color: { type: string }
                font: { type: string, enum: [Inter, Roboto, Lato, Montserrat, "Open Sans", Outfit, "Plus Jakarta Sans"] }
                website_url: { type: string, format: uri }
                auto_fetch_from_url: { type: string, format: uri, description: Read logo and primary color from this site; explicit fields win. }
      responses:
        "200": { description: Updated }
        "422": { description: Validation failed }
  /orgs/{org_id}/embed:
    parameters:
      - { name: org_id, in: path, required: true, schema: { type: string } }
    get:
      summary: The iframe snippet and hosted-page URL
      responses:
        "200": { description: OK }
  /orgs/{org_id}/hosted-page:
    parameters:
      - { name: org_id, in: path, required: true, schema: { type: string } }
    put:
      summary: Enable and fill the hosted page
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/HostedPage" }
      responses:
        "200": { description: Updated }
        "422": { description: Validation failed }
  /orgs/{org_id}/resend-setup:
    parameters:
      - { name: org_id, in: path, required: true, schema: { type: string } }
    post:
      summary: Re-send the set-password email (max 3 per 24h)
      responses:
        "200": { description: Sent }
        "429": { description: Limit reached }
