Documentation

Changelog

Dated record of every change to the API and the product.

Changes a caller could observe: new endpoints, changed responses, new refusal codes, altered headers. Internal work that no client can see is not listed, because a changelog nobody can verify is not a record.

This record begins on 2026-08-27. Changes made before that date are not reconstructed here — we would be dating them by inference, and an invented date on a changelog is worse than an absent one. Entries are seeded only where the change is attested in the shipped server and can be dated from it.

Entries marked Breaking change what an existing correct client receives. The notice you are owed before one of those ships is stated in Versioning and deprecation; where a breaking entry needs migration steps, they go in Migration guides.

2026

Added

The website editor gained its own refusal vocabulary.

Editing now declines with named reasons rather than a generic failure — including unbacked_claim, which applies the evidence rule to copy: wording your product record cannot back is refused rather than written. Version navigation, sections, experiments and proposals each got their own unknown_* code.

Affects: POST /api/ai/edit, GET /api/reasons

Fixed

Session cookies are now set on statically served funnel pages.

Funnel-entry paths are served by the static file handler, which had no hook to carry a Set-Cookie. Visitors arriving through those paths were not issued a session until their first API call. The cookie is now flushed through the one place every response of this server passes.

Affects: ff_session

Changed

Every route now answers a crash with JSON.

One dispatch guard wraps every handler. An uncaught exception was previously answered with the standard library's HTML error page, which a JSON client cannot parse; it is now 500 with {"status":"error","reason":"internal"}. Clients no longer need a non-JSON fallback on /api/ paths.

Affects: every /api/ route

Breaking

A verified email address can no longer be bound from a typed form.

POST /api/account now refuses with 409 when the address already belongs to a verified account, and sends a sign-in link to it instead. Before this, a typed address was believed outright: anyone could type a founder's email at the account step and adopt their account, claims and meter. Callers must handle 409 and direct the person to their inbox.

Affects: POST /api/account

Changed

Choosing a candidate is no longer blocked by another founder's hold.

Only a claim blocks a selection. Refusing a candidate because someone else was merely looking at it blocked founders from choosing something nobody owned; the remaining race is answered with claimed_by_another_founder plus what you still have.

Affects: POST /api/select

Changed

Responses are cacheable by route, and /api/ never is.

With the origin moved behind a CDN, everything under /api/ and /login is served private, no-store, must-revalidate — per-account state cached at an edge would be served to the wrong founder. The application shell is no-cache; static assets carry a real TTL.

Affects: Cache-Control on every response

Added

Supplier discovery returns choosable suppliers, or none and why.

GET /api/suppliers searches by your own product by default and honours a typed term. The upstream source publishes no API, so when parsing fails the endpoint returns zero suppliers and says which — never a plausible factory, because a founder emails these companies.

Affects: GET /api/suppliers

Added

The curriculum layer abstains rather than answers unsupported.

not_ingested, no_query_terms and no_passage_meets_threshold were added to the refusal registry when this endpoint was wired. The registry's bidirectional check caught them as emitted-but-undeclared.

Affects: GET /api/curriculum, GET /api/reasons

Added

3D meshes are built on demand, and their absence is stated.

A cached mesh returns immediately; an unbuilt one is reconstructed by a resident worker. Where no worker is running the endpoint refuses with no_worker rather than failing silently, so a client does not render a control that cannot work.

Affects: GET /api/model

Added

Per-feature usage metering, tiers and the referral multiplier.

AI-backed features are metered in credits against per-feature buckets over two nested windows, with a Free / Pro / Max 5x / Max 20x ladder and a referral multiplier capped at 4x. Exhaustion answers 429 naming the feature, the reset time and the features still unaffected. Before this, the AI editor ran unlimited with no account behind it.

Affects: GET /api/usage, POST /api/ai/edit, GET /api/find

Added

Sign-in by emailed link.

Single-use tokens with a fifteen-minute lifetime, redeemed at GET /login, which answers 303 and sets the session cookie on both outcomes. Expired, spent and never-existed all answer invalid_link, so the endpoint is not a membership oracle.

Affects: POST /api/login/request, GET /login, POST /api/logout

Watching for changes

The refusal registry is the one part of the interface you can diff mechanically. GET /api/reasons returns every code the server can emit; a new code appearing there is a change your client may need to handle, and a code disappearing is one it can stop handling.

# run in CI: fail when the served vocabulary drifts from what you handle
curl -s "$FLOWFINDS_BASE/api/reasons" \
  | python3 -c 'import json,sys; print("\n".join(sorted(json.load(sys.stdin)["reasons"])))' \
  > reasons.observed
diff -u reasons.expected reasons.observed

Product and company announcements — as distinct from interface changes — are published on News.

Next: Versioning and deprecation · Migration guides · API reference