Skip to content

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/markdown to any page URL, or fetch its index.md sibling. 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.

MethodPathoperationIdPurpose
POST/api/contactsubmitContactEnquiryDeliver an enquiry to the KUZOG team by email.
GET/api/healthgetHealthLiveness 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:

FieldRequiredMax length
nameyes120
emailyes200
messageyes5000
topicno80
industryno80
cf-turnstile-responseyesTurnstile token
botcheckmust be empty0
Send an enquiry
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.

Check the service
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.

Error envelope
{
  "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"
  ]
}
CodeStatusMeaningWhat to do
malformed-request400The request body could not be read as form data.Send a multipart/form-data or application/x-www-form-urlencoded body.
invalid-fields400A required field is missing or the email address is not valid.Provide non-empty name and message fields and a syntactically valid email.
missing-captcha400No 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-failed400Cloudflare 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-unreachable502The Turnstile verification service could not be reached.This is transient. Retry with a fresh token after a short wait, or email [email protected].
send-failed502The enquiry was validated but the email could not be sent.Retry later with a fresh Turnstile token, or email [email protected] directly.
rate-limited429Too 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-allowed405This endpoint does not support the request method.Use one of the methods listed in the Allow header.
not-found404No 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:

HeaderExampleMeaning
RateLimit-Policy"contact";q=10;w=60The policy: q requests per window of w seconds. Constant.
RateLimit"contact";r=9;t=60r requests remaining, t seconds until the oldest counted request leaves the window (0 when none is counted).
X-RateLimit-Limit10Requests allowed per window.
X-RateLimit-Remaining9Requests remaining in the current window.
X-RateLimit-Reset60Seconds until the window resets. A delta, not a Unix timestamp.
Retry-After42On 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.

SignalWhereMeaning
API-VersionEvery /api/ response; info.x-api-version in openapi.jsonThe contract date the response was produced under.
DeprecationResponses of a shape scheduled for removalWhen the deprecation was announced. Absent today.
SunsetSame responsesWhen 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.

Ask for Markdown
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 q wins. On a tie, the type listed first wins; */* and text/* alone mean HTML, the default. A missing Accept means 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 Accept rules out both text/html and text/markdown (for example Accept: application/json), the answer is 406 Not Acceptable with a plain-text body listing the two available representations.
  • Only page URLs negotiate: GET or HEAD, outside /api/, with no file extension. /llms.txt, /sitemap.xml, /openapi.json and 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.

Or fetch the sibling directly
curl -sS https://www.kuzog.com/microplantes/index.md

Agent files

The files to read first, in the order an agent usually needs them. All absolute, all on the canonical host, all public.

URLPurpose
https://www.kuzog.com/llms.txtShort 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.txtEvery product, process, result and FAQ answer from the site in one Markdown file.
https://www.kuzog.com/openapi.jsonOpenAPI 3.1 description of the HTTP API on this page.
https://www.kuzog.com/sitemap.xmlEvery indexable page URL with lastmod, on the canonical www host.
https://www.kuzog.com/robots.txtCrawl 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.txtllms-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.

TypeWhereNotes
OrganizationEvery pageKUZOG France, with Hydrobio and Microplantes as subOrganization, a ContactPoint (email) and a PostalAddress.
WebSite/The site node, linked to the organisation.
BreadcrumbListEvery 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.

DateChange
2026-08-22Initial publication: JSON error envelope and rate-limit headers on /api/, GET /api/health, OpenAPI 3.1 description, Markdown content negotiation, this page.