KUZOG · Developers
Developer documentation
How to read kuzog.com as a machine and how to reach KUZOG through it. One small HTTP API, one OpenAPI description, Markdown on every page, and the agent files that tie them together.
Overview
kuzog.com is a static site served from Cloudflare Pages, with a handful of Pages Functions behind /api/. It is built to be read without a browser:
- Every route is prerendered to HTML at build time, so the full content of a page is in the initial markup. No JavaScript is needed to read it.
- Every page is also available as Markdown: send
Accept: text/markdownto any page URL, or fetch itsindex.mdsibling. See Markdown content negotiation. - llms.txt is a short brief and llms-full.txt the complete content reference, both generated from the site's own copy.
- sitemap.xml lists every indexable URL; robots.txt allows everything and names the major AI crawlers explicitly.
- The HTTP API is described by openapi.json (OpenAPI 3.1) and documented on this page.
- Every page carries schema.org JSON-LD: the organisation, its two ventures, products, FAQs, articles and open roles.
The canonical host is www.kuzog.com; the apex kuzog.com answers 301 to it. Use the www form in every request.
Authentication
There is none. No API keys exist, no OAuth, no tokens to request. Everything on the site, including the API description, is public and unauthenticated.
The one write operation, POST /api/contact, is protected by Cloudflare Turnstile instead. A valid cf-turnstile-response token is required on every submission, and a token is issued only when a human completes the challenge in a browser on www.kuzog.com. Tokens are single-use and expire within minutes. There is no server-to-server route around this, by design: the form delivers email to a person, and the challenge is what keeps it usable.
What this means for an agent: you cannot submit the form autonomously. Hand off to a human instead — open https://www.kuzog.com/contact/ for the user with the message drafted, or tell them to email [email protected]. A submission without a token is answered with missing-captcha; a spent or expired token with captcha-failed.
Endpoints
Request bodies: the browser form posts multipart/form-data; from a function-calling schema post an application/json object with the same field names instead. Validation and responses are identical for both.
Two endpoints, both under https://www.kuzog.com/api/, both JSON in every response. The full description with schemas is at https://www.kuzog.com/openapi.json.
| Method | Path | operationId | Purpose |
|---|---|---|---|
| POST | /api/contact | submitContactEnquiry | Deliver an enquiry to the KUZOG team by email. |
| GET | /api/health | getHealth | Liveness probe; reports rate-limit standing without spending quota. |
POST /api/contact takes multipart/form-data (URL-encoded works too). Required: name, email, message, cf-turnstile-response. Optional: topic (shown in the subject line), industry, submitted_at. botcheck is a honeypot and must be empty or absent. Fields are trimmed, stripped of control characters and cut at their maximum length:
| Field | Required | Max length |
|---|---|---|
name | yes | 120 |
email | yes | 200 |
message | yes | 5000 |
topic | no | 80 |
industry | no | 80 |
cf-turnstile-response | yes | Turnstile token |
botcheck | must be empty | 0 |
curl -sS -X POST https://www.kuzog.com/api/contact \
-F 'name=Ada Lovelace' \
-F '[email protected]' \
-F 'topic=Hydrobio' \
-F 'message=We farm 40 ha of sandy soil and would like to trial Hydrobio on one parcel.' \
-F 'cf-turnstile-response=<token solved in the browser>'
# 200
{"success":true}A delivered enquiry answers 200 {"success":true} and sets the sender as reply-to, so the team answers them directly. A filled honeypot also answers 200 and sends nothing; the two are deliberately indistinguishable.
curl -sS https://www.kuzog.com/api/health
# 200
{
"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"
}/api/health proves the Functions are deployed and answering. It does not call Turnstile or the email provider, so a 200 there says the route is up, not that a given submission will be delivered. Any other method on either endpoint answers 405 with an Allow header; any other path under /api/ answers 404. Both are JSON.
Errors
Every error under /api/ uses one envelope. error is a stable machine code to branch on; codes are added, never renamed. message says what went wrong, hint what to do about it, docs links here, and status repeats the HTTP status so a logged body stands on its own. codes appears only on captcha-failed and carries Turnstile’s own error codes.
{
"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"
]
}| Code | Status | Meaning | What to do |
|---|---|---|---|
malformed-request | 400 | The request body could not be read as form data. | Send a multipart/form-data or application/x-www-form-urlencoded body. |
invalid-fields | 400 | A required field is missing or the email address is not valid. | Provide non-empty name and message fields and a syntactically valid email. |
missing-captcha | 400 | No Turnstile token was included in the request. | A human must solve the Cloudflare Turnstile challenge in a browser; send its token as cf-turnstile-response. |
captcha-failed | 400 | Cloudflare Turnstile rejected the token. | Inspect the codes array: timeout-or-duplicate means the token expired or was already used, so solve the challenge again and resubmit. |
captcha-unreachable | 502 | The Turnstile verification service could not be reached. | This is transient. Retry with a fresh token after a short wait, or email [email protected]. |
send-failed | 502 | The enquiry was validated but the email could not be sent. | Retry later with a fresh Turnstile token, or email [email protected] directly. |
rate-limited | 429 | Too many requests from this address in the current window. | Wait for the number of seconds in the Retry-After header, then try again. |
method-not-allowed | 405 | This endpoint does not support the request method. | Use one of the methods listed in the Allow header. |
not-found | 404 | No API endpoint exists at this path. | See the OpenAPI description at https://www.kuzog.com/openapi.json for the available endpoints. |
The Turnstile codes worth knowing: timeout-or-duplicate means the token expired or was already used, so solve the challenge again and resubmit; invalid-input-response means the token was never valid. Successful responses are always {"success":true} and nothing else.
Rate limits
One policy, named contact: 10 requests per 60 seconds per client IP address on POST /api/contact, as a sliding window. Only contact submissions count; /api/health, 405 and 404 responses report your standing without spending it.
Every /api/ response carries the policy and your current standing, in the IETF structured-field form (draft-ietf-httpapi-ratelimit-headers) and in the legacy X-RateLimit-* trio for clients that only know those:
| Header | Example | Meaning |
|---|---|---|
RateLimit-Policy | "contact";q=10;w=60 | The policy: q requests per window of w seconds. Constant. |
RateLimit | "contact";r=9;t=60 | r requests remaining, t seconds until the oldest counted request leaves the window (0 when none is counted). |
X-RateLimit-Limit | 10 | Requests allowed per window. |
X-RateLimit-Remaining | 9 | Requests remaining in the current window. |
X-RateLimit-Reset | 60 | Seconds until the window resets. A delta, not a Unix timestamp. |
Retry-After | 42 | On a 429 only: seconds to wait. Never 0. |
Over the limit, the request is refused with 429 and the rate-limited envelope before anything is read or verified, so it costs neither side anything. Refused requests are not counted, so a client that waits Retry-After seconds is let back in as the window moves on.
The count is best-effort: it lives in the memory of the Cloudflare edge isolate that serves the request, and there are many isolates in many locations. Two requests from one address may be counted separately, and the count resets when an isolate is recycled. That is deliberate. Turnstile is the real gate against automation; this limit only stops one address from turning a single isolate into a firehose. Treat the headers as advisory and the 429 as authoritative.
Versioning and deprecation
The API is date-versioned. Every /api/ response carries API-Version: 2026-08-22 — the date of the last change to any request or response shape. Record the value you integrated against; a newer date on a later response means the contract has changed and the changelog says how.
Breaking changes never replace a shape in place. They ship under a new date, and the previous shape keeps answering for at least 180 days. During that period every response of the old shape carries Deprecation: @<unix-seconds> (RFC 9745) and Sunset: <HTTP-date> (RFC 8594) and a Link: <https://www.kuzog.com/docs/#versioning>; rel="deprecation". Additive changes — a new optional field, a new endpoint, a new error code — do not bump the date and do not deprecate anything.
| Signal | Where | Meaning |
|---|---|---|
API-Version | Every /api/ response; info.x-api-version in openapi.json | The contract date the response was produced under. |
Deprecation | Responses of a shape scheduled for removal | When the deprecation was announced. Absent today. |
Sunset | Same responses | When the shape stops answering — at least 180 days after Deprecation. Absent today. |
Nothing is deprecated today. There is no version segment in the URL path: /api/contact and /api/health are the only paths, and they stay.
Markdown content negotiation
Every page URL has two representations, HTML and Markdown, chosen by the Accept header. The Markdown is generated at build time alongside the HTML, so the two never drift.
curl -sS -H 'Accept: text/markdown' https://www.kuzog.com/hydrobio/
# 200
# Content-Type: text/markdown; charset=utf-8
# Content-Location: /hydrobio/index.md
# Link: <https://www.kuzog.com/hydrobio/>; rel="alternate"; type="text/html"
# Vary: Accept- The highest
qwins. On a tie, the type listed first wins;*/*andtext/*alone mean HTML, the default. A missingAcceptmeans HTML. - Every negotiated response carries
Vary: Accept, so a cache never hands one client’s representation to another. - HTML responses carry
Link: <…/index.md>; rel="alternate"; type="text/markdown", pointing at the Markdown sibling; Markdown responses link back to the HTML. - If
Acceptrules out bothtext/htmlandtext/markdown(for exampleAccept: application/json), the answer is406 Not Acceptablewith a plain-text body listing the two available representations. - Only page URLs negotiate:
GETorHEAD, outside/api/, with no file extension./llms.txt,/sitemap.xml,/openapi.jsonand assets are served as-is.
Prefer a plain URL? Each page has a Markdown sibling at <page>/index.md: https://www.kuzog.com/index.md for the home page, https://www.kuzog.com/hydrobio/index.md, https://www.kuzog.com/blog/<slug>/index.md, and so on. Fetch it with no special header.
curl -sS https://www.kuzog.com/microplantes/index.mdAgent files
The files to read first, in the order an agent usually needs them. All absolute, all on the canonical host, all public.
| URL | Purpose |
|---|---|
| https://www.kuzog.com/llms.txt | Short brief: what KUZOG is, the two ventures, key figures, when to use the site, how to call the API. Start here. |
| https://www.kuzog.com/llms-full.txt | Every product, process, result and FAQ answer from the site in one Markdown file. |
| https://www.kuzog.com/openapi.json | OpenAPI 3.1 description of the HTTP API on this page. |
| https://www.kuzog.com/sitemap.xml | Every indexable page URL with lastmod, on the canonical www host. |
| https://www.kuzog.com/robots.txt | Crawl policy. Everything is allowed; the major AI crawlers are named explicitly. |
| https://www.kuzog.com/docs/ | This page. Also readable as Markdown with Accept: text/markdown. |
Brief first, then depth: llms.txt → llms-full.txt → the page itself as Markdown. To act on a user’s behalf, read the Authentication section before calling the API.
Structured data
Every prerendered page embeds schema.org JSON-LD in <script type="application/ld+json"> blocks, generated from the same English content the page renders. Nothing in it is invented: every string is a stable identifier or lifted from the page copy.
| Type | Where | Notes |
|---|---|---|
Organization | Every page | KUZOG France, with Hydrobio and Microplantes as subOrganization, a ContactPoint (email) and a PostalAddress. |
WebSite | / | The site node, linked to the organisation. |
BreadcrumbList | Every page except / | Home → page; articles are Home → Blog → article. |
Product | /hydrobio/ | Hydrobio as a product with its headline figures as additionalProperty, linked to its Brand and maker. |
Organization | /microplantes/ | Microplantes as a laboratory, not a packaged good. |
FAQPage | /hydrobio/, /microplantes/ | Each page’s FAQ as Question / Answer pairs. |
Blog, ItemList | /blog/ | The journal and an ordered list of every article URL. |
BlogPosting | /blog/<slug>/ | One per article; author and publisher are the organisation. |
JobPosting | /careers/ | One per open role, from the same data the page lists. |
The @id of every node is an absolute URL on www.kuzog.com, so a node on one page can be referenced from another.
Changelog
2026-08-22 (later the same day) — POST /api/contact accepts application/json bodies; every /api/ response carries API-Version; versioning and deprecation policy published; Markdown 404 bodies for agents on unknown page URLs.
| Date | Change |
|---|---|
| 2026-08-22 | Initial publication: JSON error envelope and rate-limit headers on /api/, GET /api/health, OpenAPI 3.1 description, Markdown content negotiation, this page. |