{
  "openapi": "3.1.0",
  "info": {
    "title": "The Decksmith Public API",
    "description": "Consultation lead intake for The Decksmith — Sydney Eastern Suburbs deck and pergola builder (NSW licence 272840C).\n\nBrowser forms continue to use application/x-www-form-urlencoded with 303 redirects. Agents should prefer Accept: application/json and/or Content-Type: application/json for JSON request/response shapes documented here. No secrets are exposed by this API.\n\n## Versioning\nCurrent API version is **1**, exposed primarily at `/api/v1/lead`. `/api/lead` is a stable alias for the same v1 handler (not a separate implementation). Breaking changes will introduce `/api/v2/...`. When a path is retired it will send `Deprecation` and `Sunset` headers (none retired yet). Successful and error responses include `API-Version: 1`.\n\n## Rate limiting\nSoft per-IP limit: **30 POSTs per 60 seconds** (in-isolate best-effort). Responses include `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Policy: \"30;w=60\"`. When exceeded: HTTP 429 JSON for JSON clients (with `Retry-After`), or 303 to `/contact/` for browser form posts.",
    "version": "1.1.0",
    "contact": {
      "name": "Greg — The Decksmith",
      "email": "greg@thedecksmith.com.au",
      "url": "https://www.thedecksmith.com.au/contact/"
    },
    "x-deprecation-policy": "Versions live in the URL path (/api/v1, /api/v2, …). Breaking changes ship as a new major path version. Retired paths advertise Deprecation and Sunset response headers before removal. No paths are retired as of this document. /api/lead remains a non-deprecated stable alias of v1."
  },
  "servers": [
    {
      "url": "https://www.thedecksmith.com.au",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Leads",
      "description": "Consultation lead intake and health"
    }
  ],
  "paths": {
    "/api/v1/lead": {
      "get": {
        "operationId": "getLeadHealthV1",
        "tags": [
          "Leads"
        ],
        "summary": "Lead endpoint health check (v1)",
        "description": "Returns JSON health status for the Consultation lead intake endpoint. Suitable for uptime monitors and agents verifying the API is reachable. Includes RateLimit-* and API-Version headers (GET does not consume the POST quota).",
        "responses": {
          "200": {
            "$ref": "#/components/responses/LeadHealthOk"
          },
          "405": {
            "$ref": "#/components/responses/LeadMethodNotAllowed"
          }
        }
      },
      "post": {
        "operationId": "submitConsultationLeadV1",
        "tags": [
          "Leads"
        ],
        "summary": "Submit a Consultation lead (v1)",
        "description": "Accepts a Consultation request from homeowners (or agents acting for them). Supports application/x-www-form-urlencoded (browser forms) and application/json (agents). Required fields: name_1, email_1. Optional: phone_1, text_1 (postcode), textarea_1 (message), botcheck (honeypot — leave empty). When the client wants JSON (Accept includes application/json, or Content-Type is application/json), success is 200 JSON and validation/parse failures are 400 JSON with `{ error: { code, message, resolution } }`. Otherwise browser clients receive 303 redirects to /thanks/ (success) or /contact/ (failure). Soft rate limit: 30 POSTs / 60s per IP → 429 JSON or 303 /contact/ with Retry-After.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/LeadRequest"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LeadRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/LeadAcceptedJson"
          },
          "303": {
            "$ref": "#/components/responses/LeadRedirect"
          },
          "400": {
            "$ref": "#/components/responses/LeadBadRequest"
          },
          "429": {
            "$ref": "#/components/responses/LeadRateLimited"
          },
          "405": {
            "$ref": "#/components/responses/LeadMethodNotAllowed"
          }
        }
      },
      "options": {
        "operationId": "optionsLeadV1",
        "tags": [
          "Leads"
        ],
        "summary": "CORS preflight for lead endpoint",
        "description": "Handles browser CORS preflight. Returns 204 with Allow-Methods GET, POST, OPTIONS.",
        "responses": {
          "204": {
            "$ref": "#/components/responses/CorsPreflight"
          }
        }
      },
      "summary": "Consultation lead intake (primary v1 path)",
      "description": "Primary versioned path. Identical behaviour to /api/lead."
    },
    "/api/lead": {
      "get": {
        "operationId": "getLeadHealth",
        "tags": [
          "Leads"
        ],
        "summary": "Lead endpoint health check (stable alias)",
        "description": "Returns JSON health status for the Consultation lead intake endpoint. Suitable for uptime monitors and agents verifying the API is reachable. Includes RateLimit-* and API-Version headers (GET does not consume the POST quota).",
        "responses": {
          "200": {
            "$ref": "#/components/responses/LeadHealthOk"
          },
          "405": {
            "$ref": "#/components/responses/LeadMethodNotAllowed"
          }
        }
      },
      "post": {
        "operationId": "submitConsultationLead",
        "tags": [
          "Leads"
        ],
        "summary": "Submit a Consultation lead (stable alias)",
        "description": "Accepts a Consultation request from homeowners (or agents acting for them). Supports application/x-www-form-urlencoded (browser forms) and application/json (agents). Required fields: name_1, email_1. Optional: phone_1, text_1 (postcode), textarea_1 (message), botcheck (honeypot — leave empty). When the client wants JSON (Accept includes application/json, or Content-Type is application/json), success is 200 JSON and validation/parse failures are 400 JSON with `{ error: { code, message, resolution } }`. Otherwise browser clients receive 303 redirects to /thanks/ (success) or /contact/ (failure). Soft rate limit: 30 POSTs / 60s per IP → 429 JSON or 303 /contact/ with Retry-After.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/LeadRequest"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LeadRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/LeadAcceptedJson"
          },
          "303": {
            "$ref": "#/components/responses/LeadRedirect"
          },
          "400": {
            "$ref": "#/components/responses/LeadBadRequest"
          },
          "429": {
            "$ref": "#/components/responses/LeadRateLimited"
          },
          "405": {
            "$ref": "#/components/responses/LeadMethodNotAllowed"
          }
        }
      },
      "options": {
        "operationId": "optionsLead",
        "tags": [
          "Leads"
        ],
        "summary": "CORS preflight for lead endpoint",
        "description": "Handles browser CORS preflight. Returns 204 with Allow-Methods GET, POST, OPTIONS.",
        "responses": {
          "204": {
            "$ref": "#/components/responses/CorsPreflight"
          }
        }
      },
      "summary": "Consultation lead intake (stable alias of v1)",
      "description": "Stable alias for `/api/v1/lead`. Same shared handler — not a duplicate implementation. Prefer `/api/v1/lead` for new agent integrations; this alias remains supported.",
      "deprecated": false,
      "x-alias-of": "/api/v1/lead"
    }
  },
  "components": {
    "schemas": {
      "LeadRequest": {
        "type": "object",
        "required": [
          "name_1",
          "email_1"
        ],
        "properties": {
          "name_1": {
            "type": "string",
            "description": "Full name of the person requesting a Consultation",
            "minLength": 1,
            "examples": [
              "Alex Example"
            ]
          },
          "email_1": {
            "type": "string",
            "format": "email",
            "description": "Contact email",
            "examples": [
              "alex@example.com"
            ]
          },
          "phone_1": {
            "type": "string",
            "description": "Contact phone (optional)",
            "examples": [
              "0468 486 290"
            ]
          },
          "text_1": {
            "type": "string",
            "description": "Postcode (optional)",
            "examples": [
              "2026"
            ]
          },
          "textarea_1": {
            "type": "string",
            "description": "Project message / notes (optional)"
          },
          "botcheck": {
            "type": "string",
            "description": "Honeypot field for bots — humans and legitimate agents must leave this empty or omit it",
            "default": ""
          }
        },
        "additionalProperties": true
      },
      "LeadHealth": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "const": "ok",
            "description": "Health indicator"
          }
        },
        "additionalProperties": false
      },
      "LeadSuccess": {
        "type": "object",
        "required": [
          "ok",
          "message"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "examples": [
              "Consultation request received"
            ]
          }
        },
        "additionalProperties": false
      },
      "ApiError": {
        "type": "object",
        "required": [
          "code",
          "message",
          "resolution"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Machine-readable error code",
            "enum": [
              "VALIDATION_ERROR",
              "PARSE_ERROR",
              "RATE_LIMITED",
              "METHOD_NOT_ALLOWED"
            ]
          },
          "message": {
            "type": "string",
            "description": "Human-readable error summary"
          },
          "resolution": {
            "type": "string",
            "description": "How to fix the request"
          },
          "details": {
            "description": "Optional structured details",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ApiErrorEnvelope": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ApiError"
          }
        },
        "additionalProperties": false
      }
    },
    "headers": {
      "RateLimitLimit": {
        "description": "Soft request limit for this client IP in the current window",
        "schema": {
          "type": "string",
          "examples": [
            "30"
          ]
        }
      },
      "RateLimitRemaining": {
        "description": "Remaining POSTs allowed in the current window (best-effort)",
        "schema": {
          "type": "string",
          "examples": [
            "29"
          ]
        }
      },
      "RateLimitPolicy": {
        "description": "RFC-ish policy token: limit;w=windowSeconds",
        "schema": {
          "type": "string",
          "examples": [
            "30;w=60"
          ]
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying (present on 429)",
        "schema": {
          "type": "string",
          "examples": [
            "60"
          ]
        }
      },
      "ApiVersion": {
        "description": "API major version served by this handler",
        "schema": {
          "type": "string",
          "const": "1"
        }
      },
      "Allow": {
        "description": "Allowed HTTP methods",
        "schema": {
          "type": "string",
          "examples": [
            "GET, POST, OPTIONS"
          ]
        }
      }
    },
    "responses": {
      "LeadHealthOk": {
        "description": "Endpoint healthy",
        "headers": {
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/LeadHealth"
            },
            "example": {
              "status": "ok"
            }
          }
        }
      },
      "LeadAcceptedJson": {
        "description": "Lead accepted (JSON clients)",
        "headers": {
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/LeadSuccess"
            },
            "example": {
              "ok": true,
              "message": "Consultation request received"
            }
          }
        }
      },
      "LeadRedirect": {
        "description": "Browser form flow redirect. Success Location: /thanks/. Failure or rate-limit Location: /contact/.",
        "headers": {
          "Location": {
            "description": "Redirect target",
            "schema": {
              "type": "string",
              "format": "uri",
              "examples": [
                "/thanks/",
                "/contact/"
              ]
            }
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        }
      },
      "LeadBadRequest": {
        "description": "Validation or parse error (JSON clients)",
        "headers": {
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiErrorEnvelope"
            },
            "examples": {
              "validation": {
                "summary": "Missing required fields",
                "value": {
                  "error": {
                    "code": "VALIDATION_ERROR",
                    "message": "name_1 and a valid email_1 are required",
                    "resolution": "Provide name_1 and a valid email_1. See /openapi.json"
                  }
                }
              },
              "parse": {
                "summary": "Unparseable body",
                "value": {
                  "error": {
                    "code": "PARSE_ERROR",
                    "message": "Could not parse request body",
                    "resolution": "Send application/json with an object body, or application/x-www-form-urlencoded. See /openapi.json",
                    "details": {
                      "reason": "JSON body must be an object"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "LeadRateLimited": {
        "description": "Soft per-IP rate limit exceeded (JSON clients)",
        "headers": {
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiErrorEnvelope"
            },
            "example": {
              "error": {
                "code": "RATE_LIMITED",
                "message": "Too many Consultation requests from this IP",
                "resolution": "Wait for Retry-After seconds, then retry. Soft limit: 30 POSTs per 60 seconds per IP.",
                "details": {
                  "limit": 30,
                  "windowSeconds": 60
                }
              }
            }
          }
        }
      },
      "LeadMethodNotAllowed": {
        "description": "HTTP method not allowed",
        "headers": {
          "Allow": {
            "$ref": "#/components/headers/Allow"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiErrorEnvelope"
            },
            "example": {
              "error": {
                "code": "METHOD_NOT_ALLOWED",
                "message": "Method not allowed",
                "resolution": "Use GET (health), POST (submit lead), or OPTIONS (CORS preflight). See /openapi.json",
                "details": {
                  "allow": [
                    "GET",
                    "POST",
                    "OPTIONS"
                  ]
                }
              }
            }
          }
        }
      },
      "CorsPreflight": {
        "description": "CORS preflight OK",
        "headers": {
          "Access-Control-Allow-Origin": {
            "schema": {
              "type": "string",
              "examples": [
                "*"
              ]
            }
          },
          "Access-Control-Allow-Methods": {
            "schema": {
              "type": "string",
              "examples": [
                "GET, POST, OPTIONS"
              ]
            }
          },
          "Access-Control-Allow-Headers": {
            "schema": {
              "type": "string",
              "examples": [
                "Content-Type, Accept"
              ]
            }
          },
          "Access-Control-Max-Age": {
            "schema": {
              "type": "string",
              "examples": [
                "86400"
              ]
            }
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        }
      }
    }
  }
}
