{
  "openapi": "3.1.0",
  "info": {
    "title": "BuildRules request API",
    "version": "1.0.0",
    "description": "BuildRules documents a read-only public municipal-source answer index and a consent-based project enquiry endpoint. Published guidance is not municipal approval or a property-specific determination."
  },
  "servers": [
    {
      "url": "https://buildrules.ca"
    }
  ],
  "paths": {
    "/api/leads": {
      "post": {
        "operationId": "submitProjectRequest",
        "summary": "Submit a project information request",
        "description": "Writes a project request to BuildRules, not directly to a contractor. Obtain explicit consent before sending personal information. The `saved` response property is the authoritative success indicator: `ok:true,saved:false` acknowledges a honeypot submission without storage.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "email",
                  "consent",
                  "intent"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Required non-empty name. Trimmed and truncated to 120 characters."
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "Required email. Trimmed, lowercased and truncated to 200 characters before validation."
                  },
                  "consent": {
                    "type": "boolean",
                    "const": true,
                    "description": "Must reflect the person's explicit consent."
                  },
                  "project": {
                    "type": "string",
                    "description": "Optional project label, trimmed and truncated to 120 characters. Used to derive a slug when projectSlug is empty."
                  },
                  "projectSlug": {
                    "type": "string",
                    "description": "Optional slug, lowercased, restricted to letters, digits and hyphens and truncated to 120 characters."
                  },
                  "jurisdiction": {
                    "type": "string",
                    "description": "Optional slug, sanitized like projectSlug. Defaults to toronto when empty."
                  },
                  "intent": {
                    "type": "string",
                    "enum": [
                      "professional_help",
                      "permit_designer",
                      "contractor_quote",
                      "structural_engineer",
                      "application_help",
                      "other_professional_help"
                    ],
                    "description": "Required request category used to qualify the professional-help enquiry."
                  },
                  "website": {
                    "type": "string",
                    "description": "Spam honeypot. Omit or leave empty. Non-empty values return ok without storing a request."
                  },
                  "phone": {
                    "type": "string",
                    "description": "Optional phone number. Trimmed and truncated to 40 characters."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Request accepted. `saved:true` means the request was stored; `saved:false` acknowledges spam honeypot without storage.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "saved"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "saved": {
                      "type": "boolean",
                      "description": "True only after a successful insert. Honeypot acknowledgements return false."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON, missing/invalid intent, missing name, invalid email, or consent not true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Respect Retry-After when returned.",
            "headers": {
              "Retry-After": {
                "description": "Wait time in seconds when rate limiting is active.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "On a rate-limited submission, the per-email rate policy of 6 requests per 3600 seconds. An additional per-IP quota may apply.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Database could not save the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "503": {
            "description": "Request backend is not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/permit-answers": {
      "get": {
        "operationId": "getPublishedPermitAnswers",
        "summary": "Read published, cited permit answers for supported Ontario municipalities",
        "description": "Read-only city/project guidance from reviewed public sources. Filter by city and project slug. Coverage is not Canada-wide: this endpoint currently reports Ontario guidance only. No parcel, private address, legal approval, or verified building design is returned. When served by Cloudflare, the endpoint applies a per-client-IP, per-location approximate limit of 120 requests per 60 seconds and returns RateLimit-Policy. Clients should respect HTTP 429 and Retry-After. Requests without Cloudflare bindings (such as local development) are not advertised as rate-limited.",
        "security": [],
        "parameters": [
          {
            "name": "city",
            "in": "query",
            "required": false,
            "description": "Optional lowercase city slug such as toronto.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]{2,80}$"
            }
          },
          {
            "name": "project",
            "in": "query",
            "required": false,
            "description": "Optional lowercase project slug such as deck-permit.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]{2,80}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Published city/project guidance with official sources and review dates. Empty arrays mean no qualifying published answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicPermitIndex"
                }
              }
            },
            "headers": {
              "API-Version": {
                "description": "Current public response schema generation. Value 1.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "Present only when a Cloudflare native per-location rate limit was applied. Do not interpret it as a global remaining quota.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Invalid city or project filter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Cloudflare native rate limiter rejected a request. The quota is approximate per Cloudflare location, not a guaranteed global count. Wait the Retry-After duration.",
            "headers": {
              "Retry-After": {
                "description": "Minimum suggested wait, in seconds, after 429.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "Enforced rate policy when a Cloudflare binding is available, such as 120;w=60.",
                "schema": {
                  "type": "string"
                }
              },
              "API-Version": {
                "description": "Public endpoint response schema generation; current value 1.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "503": {
            "description": "Published data temporarily unavailable. The API returns JSON with an error field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ApiError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable explanation of the failed request."
          }
        },
        "additionalProperties": true
      },
      "CitedSource": {
        "type": "object",
        "required": [
          "authority",
          "title",
          "url",
          "reviewedAt"
        ],
        "properties": {
          "authority": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "reviewedAt": {
            "type": "string",
            "description": "Date the cited official source was reviewed."
          }
        },
        "additionalProperties": false
      },
      "CitedPermitAnswer": {
        "type": "object",
        "required": [
          "city",
          "municipality",
          "project",
          "question",
          "answer",
          "answerState",
          "reviewedAt",
          "guideUrl",
          "sources"
        ],
        "properties": {
          "city": {
            "type": "string"
          },
          "municipality": {
            "type": "string"
          },
          "project": {
            "type": "string"
          },
          "question": {
            "type": "string"
          },
          "answer": {
            "type": "string"
          },
          "answerState": {
            "type": "string"
          },
          "reviewedAt": {
            "type": "string"
          },
          "guideUrl": {
            "type": "string",
            "format": "uri"
          },
          "sources": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CitedSource"
            }
          }
        },
        "additionalProperties": false
      },
      "PublicPermitIndex": {
        "type": "object",
        "required": [
          "name",
          "description",
          "publisher",
          "methodology",
          "humanReadableIndex",
          "jurisdiction",
          "lastSourceReview",
          "count",
          "entries",
          "resourceGuideCount",
          "resourceGuides",
          "totalCitableAnswers"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "publisher": {
            "type": "string"
          },
          "methodology": {
            "type": "string",
            "format": "uri"
          },
          "humanReadableIndex": {
            "type": "string",
            "format": "uri"
          },
          "jurisdiction": {
            "type": "string"
          },
          "lastSourceReview": {
            "type": [
              "string",
              "null"
            ]
          },
          "count": {
            "type": "integer",
            "minimum": 0
          },
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CitedPermitAnswer"
            }
          },
          "resourceGuideCount": {
            "type": "integer",
            "minimum": 0
          },
          "resourceGuides": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "totalCitableAnswers": {
            "type": "integer",
            "minimum": 0
          }
        },
        "additionalProperties": false
      }
    }
  }
}
