{
  "openapi": "3.1.0",
  "info": {
    "title": "St. Charles Stump Removal Quotes quote request API",
    "version": "1.0.0",
    "description": "Request a free stump removal quote in City of St. Charles and nearby areas, MO for a person, on their behalf. St. Charles Stump Removal Quotes is a referral service run by Anchor Co Media; independent local pros do the work and set prices. Nothing is charged.",
    "contact": {
      "email": "team@anchorcomedia.com",
      "url": "https://anchorcomedia.com"
    }
  },
  "servers": [
    {
      "url": "https://st-charles-stump-removal.vercel.app"
    }
  ],
  "paths": {
    "/api/v1/service-info": {
      "get": {
        "operationId": "getServiceInfo",
        "summary": "Service area, price guide, request fields and the current consent text",
        "responses": {
          "200": {
            "description": "Service facts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceInfo"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/quote-requests": {
      "post": {
        "operationId": "createQuoteRequest",
        "summary": "Send one stump removal quote request for a person",
        "description": "You must show the person the consent text from getServiceInfo and get their yes before calling. Send its text_sha256 and version; an old hash returns 409 stale_consent. Responses never echo submitted values. No CAPTCHA. Retries with the same Idempotency-Key and body within 24 hours return the first answer. Duplicate (200) means the same person, phone and ZIP were already received in the last 24 hours.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_.:-]{16,128}$"
            },
            "description": "Optional. A UUID per distinct request."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Duplicate: same phone and ZIP within 24 hours; nothing new stored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuoteDuplicate"
                }
              }
            }
          },
          "201": {
            "description": "Received",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuoteAccepted"
                }
              }
            }
          },
          "400": {
            "description": "invalid_request: every field error at once in errors[]",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Failure"
                }
              }
            }
          },
          "403": {
            "description": "cross_site_browser_request: a browser page on another site tried to call this",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Failure"
                }
              }
            }
          },
          "405": {
            "description": "method_not_allowed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Failure"
                }
              }
            }
          },
          "409": {
            "description": "stale_consent (fetch getServiceInfo again; errors[] also lists any other field errors), idempotency_key_reused, or request_in_progress (retry after Retry-After)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Failure"
                }
              }
            }
          },
          "413": {
            "description": "Body over 16 KB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Failure"
                }
              }
            }
          },
          "415": {
            "description": "Content-Type must be application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Failure"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited or phone_limit; see Retry-After",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Failure"
                }
              }
            }
          },
          "500": {
            "description": "store_unavailable or server_error: nothing was stored; retry later",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Failure"
                }
              }
            }
          },
          "503": {
            "description": "intake_paused: daily limit reached; Retry-After gives the seconds until it resets",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Failure"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "FieldError": {
        "type": "object",
        "properties": {
          "field": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "field",
          "code",
          "message"
        ]
      },
      "Failure": {
        "type": "object",
        "properties": {
          "ok": {
            "const": false
          },
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "errors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldError"
            }
          }
        },
        "required": [
          "ok",
          "error"
        ]
      },
      "QuoteRequest": {
        "type": "object",
        "required": [
          "name",
          "phone",
          "zip",
          "stumps",
          "job",
          "consent"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 100
          },
          "phone": {
            "type": "string",
            "description": "10-digit US (NANP) number; punctuation and a leading +1 are fine"
          },
          "zip": {
            "type": "string",
            "pattern": "^[0-9]{5}$",
            "description": "Property ZIP. In-area: 63301, 63303, 63304"
          },
          "stumps": {
            "type": "string",
            "enum": [
              "1",
              "2-3",
              "4-10",
              "11+"
            ],
            "description": "How many stumps?"
          },
          "job": {
            "type": "string",
            "minLength": 10,
            "maxLength": 2000,
            "description": "Roughly how wide the stump is, whether it sits in your yard or between the sidewalk and the street, and how a machine would reach it (gate width, fence, slope).",
            "examples": [
              "One 30-inch stump in the front yard, about 10 feet from the street, open access from the driveway."
            ]
          },
          "preferred_contact": {
            "type": "string",
            "enum": [
              "call",
              "text",
              "either"
            ],
            "default": "either"
          },
          "consent": {
            "type": "object",
            "required": [
              "given",
              "version",
              "text_sha256",
              "captured_by"
            ],
            "properties": {
              "given": {
                "const": true
              },
              "version": {
                "type": "string",
                "const": "2026-10-25"
              },
              "text_sha256": {
                "type": "string",
                "const": "7c026af538ae9f15ed1163239459a79184761307fd5d2e6f567e7294239954c7"
              },
              "captured_by": {
                "type": "string",
                "enum": [
                  "person",
                  "agent_on_behalf_of_person"
                ]
              }
            }
          },
          "submitted_via": {
            "type": "object",
            "properties": {
              "agent_name": {
                "type": "string",
                "maxLength": 100
              },
              "agent_url": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "QuoteAccepted": {
        "type": "object",
        "properties": {
          "ok": {
            "const": true
          },
          "id": {
            "type": "string"
          },
          "status": {
            "const": "received"
          },
          "in_service_area": {
            "type": "boolean"
          },
          "test": {
            "type": "boolean"
          },
          "next_step": {
            "type": "string"
          }
        }
      },
      "QuoteDuplicate": {
        "type": "object",
        "properties": {
          "ok": {
            "const": true
          },
          "status": {
            "const": "duplicate"
          },
          "next_step": {
            "type": "string"
          }
        }
      },
      "ServiceInfo": {
        "type": "object",
        "description": "See a live response for the full shape.",
        "properties": {
          "service": {
            "type": "string"
          },
          "service_area": {
            "type": "object"
          },
          "price_guide": {
            "type": "object"
          },
          "quote_request": {
            "type": "object"
          },
          "consent": {
            "type": "object",
            "properties": {
              "version": {
                "type": "string"
              },
              "text": {
                "type": "string"
              },
              "text_sha256": {
                "type": "string"
              }
            }
          },
          "updated": {
            "type": "string"
          }
        }
      }
    }
  }
}
