{
  "openapi": "3.0.3",
  "info": {
    "title": "EstimateHawk Bid Comparison API",
    "version": "1.0.0",
    "description": "Prepaid report API for platforms and AI agents. Submit 2–5 contractor estimates, poll until the graded comparison is ready, and inspect remaining credits. One credit per delivered report; failed reports refund the credit automatically. Bearer API keys look like `eh_live_` followed by 48 hex characters. The same operations are also available as a remote MCP server at https://estimatehawk.com/mcp.\n\nThe Supabase function URL `https://zpjeqlmgwbpwkzzshwss.supabase.co/functions/v1/api-report` continues to work (POST /, GET /?id=, GET /account). This spec documents the first-party aliases on estimatehawk.com.",
    "contact": {
      "name": "EstimateHawk API support",
      "email": "support@estimatehawk.com",
      "url": "https://estimatehawk.com/api/"
    },
    "license": {
      "name": "Proprietary — see EstimateHawk Terms of Service",
      "url": "https://estimatehawk.com/terms/"
    }
  },
  "servers": [
    {
      "url": "https://estimatehawk.com/api",
      "description": "First-party EstimateHawk host"
    }
  ],
  "tags": [
    {
      "name": "Reports",
      "description": "Submit bids and fetch the graded comparison."
    },
    {
      "name": "Account",
      "description": "The API key is the account: balance, caps, and usage."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/report": {
      "post": {
        "operationId": "submit_bids",
        "tags": ["Reports"],
        "summary": "Submit 2–5 estimates for a graded comparison",
        "description": "Validates the key, file count, types, and page/byte caps, consumes one prepaid credit, and enqueues the report. Returns immediately with `202` and a `report_id`. Poll `get_report` until `status` is `completed` or `failed` (typically 1–3 minutes). A report that fails on EstimateHawk's side refunds the credit automatically.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubmitBidsRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Report accepted and processing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubmitBidsAccepted"
                }
              }
            }
          },
          "400": {
            "description": "Invalid body, file count, file entry, unsupported type, decode failure, oversize file, or too many PDF pages.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "402": {
            "description": "No report credits remaining on this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "HEIC photo, unreadable PDF, or bytes that are not a PDF/JPG/PNG/WebP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "This key's daily report cap is reached (default 50; resets at midnight UTC).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Internal error. If a credit was consumed and persistence failed, it is refunded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "503": {
            "description": "Global daily API processing capacity reached. No credit is used when this is refused before consume; if it is hit after enqueue, the credit is refunded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "get_report",
        "tags": ["Reports"],
        "summary": "Fetch a report by id",
        "description": "Same Bearer key that created the report. Always HTTP 200 when the report exists and has not expired: poll until `status` is `completed` (full `report` object) or `failed` (`error`, `error_code`, `credit_refunded: true`). Intermediate states (`uploading`, `pending`, `processing`) are returned as `status: \"processing\"`.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "Report UUID returned by `submit_bids`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Report exists on this key (processing, completed, or failed).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/ReportProcessing" },
                    { "$ref": "#/components/schemas/ReportCompleted" },
                    { "$ref": "#/components/schemas/ReportFailed" }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing or malformed `id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No report with that id on this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "410": {
            "description": "Report has passed its 90-day retention window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Report lookup failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/report/account": {
      "get": {
        "operationId": "get_account",
        "tags": ["Account"],
        "summary": "Key balance, caps, and usage",
        "description": "The key is the account. Returns the key prefix (never the secret), daily report cap, remaining credits, and usage buckets.",
        "responses": {
          "200": {
            "description": "Account snapshot for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Account or usage lookup failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "eh_live",
        "description": "API key issued after a credit-pack purchase. Send `Authorization: Bearer eh_live_<48 hex chars>` on every request. Get a key at https://estimatehawk.com/api/."
      }
    },
    "schemas": {
      "SubmitFile": {
        "type": "object",
        "required": ["name", "content_base64"],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "description": "Filename with extension. PDF estimates, or photographed quotes as JPG, PNG, or WebP. HEIC is rejected."
          },
          "content_base64": {
            "type": "string",
            "minLength": 1,
            "description": "File bytes as base64. A `data:` URL prefix is accepted and stripped. PDF up to 15 pages and 10MB; images up to 5MB."
          }
        }
      },
      "SubmitBidsRequest": {
        "type": "object",
        "required": ["files"],
        "properties": {
          "files": {
            "type": "array",
            "minItems": 2,
            "maxItems": 5,
            "items": {
              "$ref": "#/components/schemas/SubmitFile"
            }
          },
          "project_type": {
            "type": "string",
            "description": "Optional project-type hint (e.g. Roof Replacement). Sanitized against a curated allowlist before market lookup; unknown values are ignored rather than rejected."
          },
          "zip": {
            "type": "string",
            "pattern": "^\\d{5}$",
            "description": "Optional US 5-digit ZIP for market grounding. Values that are not exactly 5 digits are ignored (not an error)."
          }
        }
      },
      "SubmitBidsAccepted": {
        "type": "object",
        "required": ["report_id", "status", "credits_remaining", "result_url"],
        "properties": {
          "report_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": ["processing"]
          },
          "credits_remaining": {
            "type": "integer",
            "minimum": 0,
            "description": "Prepaid credits left on this key after consuming one for this report."
          },
          "result_url": {
            "type": "string",
            "format": "uri",
            "description": "Convenience GET URL for `get_report` on the first-party host, e.g. https://estimatehawk.com/api/report?id={report_id}. Poll with the same Authorization header."
          },
          "note": {
            "type": "string",
            "description": "Human-readable polling hint. Reports typically finish in 1–3 minutes."
          }
        }
      },
      "ReportProcessing": {
        "type": "object",
        "required": ["report_id", "status"],
        "properties": {
          "report_id": { "type": "string", "format": "uuid" },
          "status": { "type": "string", "enum": ["processing"] },
          "created_at": { "type": "string", "format": "date-time" },
          "credits_remaining": { "type": "integer", "minimum": 0 }
        }
      },
      "ReportCompleted": {
        "type": "object",
        "required": ["report_id", "status", "report"],
        "properties": {
          "report_id": { "type": "string", "format": "uuid" },
          "status": { "type": "string", "enum": ["completed"] },
          "created_at": { "type": "string", "format": "date-time" },
          "completed_at": { "type": "string", "format": "date-time" },
          "credits_remaining": { "type": "integer", "minimum": 0 },
          "report": { "$ref": "#/components/schemas/GradedReport" }
        }
      },
      "ReportFailed": {
        "type": "object",
        "required": ["report_id", "status", "credit_refunded"],
        "properties": {
          "report_id": { "type": "string", "format": "uuid" },
          "status": { "type": "string", "enum": ["failed"] },
          "created_at": { "type": "string", "format": "date-time" },
          "credits_remaining": { "type": "integer", "minimum": 0 },
          "error": { "type": "string" },
          "error_code": { "$ref": "#/components/schemas/ErrorCode" },
          "credit_refunded": {
            "type": "boolean",
            "enum": [true],
            "description": "Always true on a failed report. Do not invent extra billing behavior."
          }
        }
      },
      "GradedReport": {
        "type": "object",
        "description": "Full unredacted analysis. Additional properties may appear; the fields below are the stable product surface.",
        "additionalProperties": true,
        "properties": {
          "contractor_scores": {
            "type": "array",
            "items": { "type": "object", "additionalProperties": true }
          },
          "quick_insights": {
            "type": "object",
            "additionalProperties": true
          },
          "risk_matrix": {
            "type": "array",
            "items": { "type": "object", "additionalProperties": true }
          },
          "overall_recommendation": { "type": "string" },
          "data_grounding": {
            "type": "object",
            "description": "Whether market and reputation scores were grounded in lookups or explicitly marked insufficient_data. Never an invented number.",
            "additionalProperties": true
          },
          "detailed_breakdown": { "type": "object", "additionalProperties": true },
          "recommendations": {
            "type": "array",
            "items": { "type": "object", "additionalProperties": true }
          }
        }
      },
      "Account": {
        "type": "object",
        "required": ["key", "credits_remaining", "usage"],
        "properties": {
          "key": {
            "type": "object",
            "required": ["prefix", "daily_report_cap", "created_at"],
            "properties": {
              "label": { "type": "string" },
              "prefix": {
                "type": "string",
                "description": "Truncated key prefix for display. The secret is never returned."
              },
              "daily_report_cap": { "type": "integer", "minimum": 1 },
              "created_at": { "type": "string", "format": "date-time" }
            }
          },
          "credits_remaining": {
            "type": "integer",
            "nullable": true,
            "minimum": 0
          },
          "usage": { "$ref": "#/components/schemas/UsageSummary" },
          "docs": { "type": "string", "format": "uri" },
          "support": { "type": "string" }
        }
      },
      "UsageSummary": {
        "type": "object",
        "required": ["today", "this_month", "all_time", "last_report_at"],
        "properties": {
          "today": {
            "type": "integer",
            "minimum": 0,
            "description": "Reports accepted so far today (UTC), including failures. This is what the daily cap counts."
          },
          "this_month": { "$ref": "#/components/schemas/UsageBucket" },
          "all_time": { "$ref": "#/components/schemas/UsageBucket" },
          "last_report_at": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "UsageBucket": {
        "type": "object",
        "required": ["accepted", "completed", "failed", "processing"],
        "properties": {
          "accepted": { "type": "integer", "minimum": 0 },
          "completed": { "type": "integer", "minimum": 0 },
          "failed": { "type": "integer", "minimum": 0 },
          "processing": { "type": "integer", "minimum": 0 }
        }
      },
      "ApiError": {
        "type": "object",
        "required": ["error", "code"],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message. Copy may change; `code` is stable."
          },
          "code": { "$ref": "#/components/schemas/ErrorCode" },
          "credits_remaining": { "type": "integer", "minimum": 0 }
        }
      },
      "ErrorCode": {
        "type": "string",
        "description": "Stable machine-readable codes from the live api-report implementation. Gateway codes appear on submit/fetch/account; processing codes appear on failed reports (`error_code`) after a credit refund.",
        "enum": [
          "unauthorized",
          "invalid_body",
          "bad_file_count",
          "invalid_file_entry",
          "heic_unsupported",
          "unsupported_file",
          "file_decode_failed",
          "file_rejected",
          "pdf_unreadable",
          "too_many_pages",
          "daily_cap_reached",
          "daily_capacity_reached",
          "insufficient_credits",
          "not_found",
          "report_expired",
          "upload_incomplete",
          "processing_timeout",
          "extraction_failed",
          "extraction_transient",
          "analysis_too_large",
          "analysis_unreadable",
          "analysis_api_error",
          "analysis_timeout",
          "internal_error"
        ]
      }
    }
  },
  "externalDocs": {
    "description": "Human docs, credit packs, and MCP connector how-to",
    "url": "https://estimatehawk.com/api/"
  }
}
