{
  "openapi": "3.1.0",
  "info": {
    "title": "KUZOG website API",
    "version": "1.0.0",
    "summary": "The small HTTP API behind www.kuzog.com: send an enquiry to KUZOG, Hydrobio or Microplantes, and check that the service is up.",
    "description": "KUZOG France is a Paris agricultural-technology group running two ventures: Hydrobio (soil regeneration) and Microplantes (in-vitro plant micropropagation). This API exists so that a person, a script or an AI agent can reach the company from the website: `POST /api/contact` delivers an enquiry to the team by email, and `GET /api/health` reports that the Functions are deployed and answering.\n\n**When to call it.** Call `submitContactEnquiry` when a user wants to contact KUZOG, ask about Hydrobio or Microplantes, request a quote, a trial, a partnership or a press contact, or apply for a role. Call `getHealth` before a submission to check the service is reachable and how much rate-limit quota the caller has left.\n\n**Authentication.** There are no API keys. The contact endpoint requires a Cloudflare Turnstile token (`cf-turnstile-response`) that must be solved by a human in a browser on www.kuzog.com; an agent cannot obtain one on its own and should hand off to a person (open https://www.kuzog.com/contact/) or fall back to email. Submissions without a valid token are rejected.\n\n**Responses.** Every response under `/api/` is JSON and carries rate-limit headers. Errors use one envelope: `{ success: false, error, message, hint, docs, status }`, where `error` is a stable machine code documented at https://www.kuzog.com/docs/#errors.\n\n**Content.** The website itself is readable as Markdown: send `Accept: text/markdown` to any page URL, or fetch https://www.kuzog.com/llms.txt for a brief.\n\n**Versioning and deprecation.** The API is date-versioned: every `/api/*` response carries `API-Version: YYYY-MM-DD`, the date of the last change to any request or response shape (currently `2026-08-22`; also `info.x-api-version` here). Breaking changes ship under a new date and never replace a shape in place; the previous shape keeps answering for at least 180 days with `Deprecation` (RFC 9745) and `Sunset` (RFC 8594) headers pointing at https://www.kuzog.com/docs/#versioning. Nothing is deprecated today.",
    "contact": {
      "name": "KUZOG",
      "email": "management@kuzog.com",
      "url": "https://www.kuzog.com/contact/"
    },
    "x-api-version": "2026-08-22",
    "x-deprecation-policy": {
      "noticeDays": 180,
      "headers": [
        "Deprecation",
        "Sunset"
      ],
      "docs": "https://www.kuzog.com/docs/#versioning"
    }
  },
  "externalDocs": {
    "description": "KUZOG developer documentation: authentication, errors, rate limits, Markdown content negotiation and agent files.",
    "url": "https://www.kuzog.com/docs/"
  },
  "servers": [
    {
      "url": "https://www.kuzog.com",
      "description": "Production. The apex kuzog.com redirects here with a 301."
    }
  ],
  "tags": [
    {
      "name": "contact",
      "description": "Sending an enquiry to the KUZOG team."
    },
    {
      "name": "meta",
      "description": "Service status."
    }
  ],
  "paths": {
    "/api/contact": {
      "post": {
        "operationId": "submitContactEnquiry",
        "tags": [
          "contact"
        ],
        "summary": "Send an enquiry to KUZOG",
        "description": "Delivers a message to the KUZOG team by email and sets the sender's address as reply-to. Use it when a user wants to contact KUZOG, Hydrobio or Microplantes: a product or pricing question, a trial or order request, a partnership, press or recruitment enquiry, or a spontaneous application.\n\nThe body is `multipart/form-data` (URL-encoded forms are accepted too) or an `application/json` object with the same field names — the JSON form is the one to use from a function-calling schema. `name`, `email` and `message` are required. `topic` and `industry` are optional context the team sees in the subject line and body. `cf-turnstile-response` is the Cloudflare Turnstile token and is required: it is issued only to a human who completes the challenge in a browser on www.kuzog.com, and is single-use and short-lived. An agent that cannot obtain one should open https://www.kuzog.com/contact/ for the user, or tell them to email management@kuzog.com.\n\n`botcheck` is a honeypot and must be empty or absent; a filled honeypot is answered with a 200 that sends nothing.\n\nRate limit: 10 requests per 60 seconds per client IP address (policy `contact`), enforced best-effort per edge location. Read the `RateLimit` headers on any response and honour `Retry-After` on a 429.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/ContactEnquiry"
              },
              "encoding": {
                "message": {
                  "contentType": "text/plain"
                }
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/ContactEnquiry"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactEnquiry"
              },
              "example": {
                "name": "Ada Lovelace",
                "email": "ada@example.com",
                "message": "We farm 40 ha of sandy soil near Marrakech and would like a Hydrobio soil analysis.",
                "topic": "Hydrobio",
                "cf-turnstile-response": "<token from the Turnstile widget>"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The enquiry was delivered (or the honeypot was filled, in which case nothing was sent and the response is deliberately indistinguishable).",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/API-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessResponse"
                },
                "example": {
                  "success": true
                }
              }
            }
          },
          "400": {
            "description": "The request was understood but refused: `malformed-request`, `invalid-fields`, `missing-captcha` or `captcha-failed` (the last carries Turnstile's own `codes`).",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/API-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "missingCaptcha": {
                    "summary": "No Turnstile token",
                    "value": {
                      "success": false,
                      "error": "missing-captcha",
                      "message": "No Turnstile token was included in the request.",
                      "hint": "A human must solve the Cloudflare Turnstile challenge in a browser; send its token as cf-turnstile-response.",
                      "docs": "https://www.kuzog.com/docs/#errors",
                      "status": 400
                    }
                  },
                  "captchaFailed": {
                    "summary": "Token expired or already used",
                    "value": {
                      "success": false,
                      "error": "captcha-failed",
                      "message": "Cloudflare Turnstile rejected the token.",
                      "hint": "Inspect the codes array: timeout-or-duplicate means the token expired or was already used, so solve the challenge again and resubmit.",
                      "docs": "https://www.kuzog.com/docs/#errors",
                      "status": 400,
                      "codes": [
                        "timeout-or-duplicate"
                      ]
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "A method other than POST. The `Allow` header names the one to use.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/API-Version"
              },
              "Allow": {
                "$ref": "#/components/headers/Allow"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests from this address in the current 60-second window (`rate-limited`). Wait `Retry-After` seconds.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/API-Version"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": "rate-limited",
                  "message": "Too many requests from this address in the current window.",
                  "hint": "Wait for the number of seconds in the Retry-After header, then try again.",
                  "docs": "https://www.kuzog.com/docs/#errors",
                  "status": 429
                }
              }
            }
          },
          "502": {
            "description": "An upstream provider failed: `captcha-unreachable` (Turnstile could not be reached) or `send-failed` (the email could not be sent). Transient; retry with a fresh token or fall back to email.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/API-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "meta"
        ],
        "summary": "Check that the API is up",
        "description": "A liveness probe. Call it before `submitContactEnquiry` to confirm the Functions are deployed and answering, and to read the caller's current rate-limit standing from the response headers without spending any quota. It does not contact Turnstile or the email provider, so a 200 here means the route is up — not that a given submission will be delivered. The body links the human documentation and this OpenAPI description.",
        "responses": {
          "200": {
            "description": "The service is answering.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/API-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                },
                "example": {
                  "status": "ok",
                  "service": "kuzog-site",
                  "time": "2026-08-22T10:00:00.000Z",
                  "docs": "https://www.kuzog.com/docs/",
                  "openapi": "https://www.kuzog.com/openapi.json"
                }
              }
            }
          },
          "405": {
            "description": "A method other than GET. The `Allow` header names the one to use.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/API-Version"
              },
              "Allow": {
                "$ref": "#/components/headers/Allow"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ContactEnquiry": {
        "type": "object",
        "description": "The fields of the enquiry form. Leading and trailing whitespace is trimmed, control characters are removed, and each field is cut at its maximum length.",
        "required": [
          "name",
          "email",
          "message",
          "cf-turnstile-response"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "description": "The sender's name, as it should appear in the email.",
            "examples": [
              "Ada Lovelace"
            ]
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 200,
            "description": "The sender's address. Becomes the reply-to of the delivered email. Must be a syntactically valid address.",
            "examples": [
              "ada@example.com"
            ]
          },
          "message": {
            "type": "string",
            "minLength": 1,
            "maxLength": 5000,
            "description": "The enquiry itself. Newlines are preserved.",
            "examples": [
              "We farm 40 ha of sandy soil near Murcia and would like to trial Hydrobio on one parcel next season."
            ]
          },
          "topic": {
            "type": "string",
            "maxLength": 80,
            "description": "Optional subject, shown in the email subject line, e.g. the venture or reason for contact.",
            "examples": [
              "Hydrobio",
              "Microplantes",
              "Press / Brand",
              "Careers"
            ]
          },
          "industry": {
            "type": "string",
            "maxLength": 80,
            "description": "Optional sector of the sender, shown in the email body.",
            "examples": [
              "Arboriculture",
              "Nursery",
              "Agricultural cooperative"
            ]
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "description": "Optional client timestamp. Accepted and ignored by the server; the website sends it."
          },
          "cf-turnstile-response": {
            "type": "string",
            "maxLength": 2048,
            "description": "REQUIRED. The Cloudflare Turnstile token issued when a human completes the challenge on www.kuzog.com. Single-use, short-lived, verified server-side; an absent token is rejected with `missing-captcha`, an invalid or spent one with `captcha-failed`. There is no way to obtain one without a browser session on the site."
          },
          "botcheck": {
            "type": "string",
            "maxLength": 0,
            "description": "Honeypot. MUST be empty or omitted. Any value marks the request as automated: it is answered with 200 and discarded."
          }
        }
      },
      "SuccessResponse": {
        "type": "object",
        "required": [
          "success"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          }
        },
        "additionalProperties": false
      },
      "ErrorResponse": {
        "type": "object",
        "description": "The single error envelope every `/api/*` route uses. `error` is the stable code to branch on; `message` and `hint` are English sentences for a log or a person; `status` repeats the HTTP status so a logged body stands alone.",
        "required": [
          "success",
          "error",
          "message",
          "hint",
          "docs",
          "status"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string",
            "description": "Machine-readable code. Stable: codes are added, never renamed.",
            "enum": [
              "malformed-request",
              "invalid-fields",
              "missing-captcha",
              "captcha-failed",
              "captcha-unreachable",
              "send-failed",
              "rate-limited",
              "method-not-allowed",
              "not-found"
            ]
          },
          "message": {
            "type": "string",
            "description": "One sentence saying what went wrong."
          },
          "hint": {
            "type": "string",
            "description": "One sentence saying what the caller should do about it."
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "const": "https://www.kuzog.com/docs/#errors",
            "description": "Where the codes are documented."
          },
          "status": {
            "type": "integer",
            "description": "The HTTP status this body was sent with.",
            "enum": [
              400,
              404,
              405,
              429,
              502
            ]
          },
          "codes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Present only on `captcha-failed`: Cloudflare Turnstile's own error codes, e.g. `timeout-or-duplicate` (token expired or already used — solve the challenge again) or `invalid-input-response`."
          }
        },
        "additionalProperties": false
      },
      "Health": {
        "type": "object",
        "required": [
          "status",
          "service",
          "time",
          "docs",
          "openapi"
        ],
        "properties": {
          "status": {
            "type": "string",
            "const": "ok"
          },
          "service": {
            "type": "string",
            "const": "kuzog-site"
          },
          "time": {
            "type": "string",
            "format": "date-time",
            "description": "Server time when the response was produced, ISO 8601 in UTC."
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "const": "https://www.kuzog.com/docs/"
          },
          "openapi": {
            "type": "string",
            "format": "uri",
            "const": "https://www.kuzog.com/openapi.json"
          }
        },
        "additionalProperties": false
      }
    },
    "headers": {
      "RateLimit-Policy": {
        "description": "The quota policy, as an RFC 8941 structured field (draft-ietf-httpapi-ratelimit-headers): policy name `contact`, `q` requests per window of `w` seconds. Always `\"contact\";q=10;w=60`.",
        "schema": {
          "type": "string",
          "examples": [
            "\"contact\";q=10;w=60"
          ]
        }
      },
      "RateLimit": {
        "description": "The caller's current standing under the policy: `r` requests remaining in the window, `t` seconds until the oldest counted request leaves it (0 when none is counted). Best-effort per edge location.",
        "schema": {
          "type": "string",
          "examples": [
            "\"contact\";r=9;t=60"
          ]
        }
      },
      "X-RateLimit-Limit": {
        "description": "Legacy form of the quota: requests allowed per window (10).",
        "schema": {
          "type": "integer",
          "examples": [
            10
          ]
        }
      },
      "X-RateLimit-Remaining": {
        "description": "Legacy form: requests remaining in the current window for this address.",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "examples": [
            9
          ]
        }
      },
      "X-RateLimit-Reset": {
        "description": "Legacy form: seconds until the window resets for this address (a delta in seconds, not a Unix timestamp; 0 when no request is counted).",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "examples": [
            60
          ]
        }
      },
      "Retry-After": {
        "description": "On a 429 only: seconds to wait before retrying. Never 0.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "examples": [
            42
          ]
        }
      },
      "Allow": {
        "description": "On a 405 only: the methods the endpoint supports.",
        "schema": {
          "type": "string",
          "examples": [
            "POST",
            "GET"
          ]
        }
      },
      "API-Version": {
        "description": "Date of the last change to any request or response shape. Compare with the value you integrated against; a newer date means the contract has changed (see https://www.kuzog.com/docs/#versioning).",
        "schema": {
          "type": "string",
          "format": "date",
          "examples": [
            "2026-08-22"
          ]
        }
      }
    }
  }
}
