The web API

Everything under /api/web/v1/ belongs to the firm whose hostname the request carries. There is no organization parameter and there never will be one: the host decides, so a door cannot be tricked into answering for somebody else's firm.

These doors serve the public website and a firm's own second server. They are not the ERP's API — that is the API hub.

Releases

Door Answers
GET /api/web/v1/releases/latest the newest published build's facts
GET /api/web/v1/releases/latest/download the file, counted

Both take an optional ?product=. Without one they mean the firm's default program — never „whatever release is newest", which would become a different program the day the firm publishes a second one.

The facts door answers anyone and returns no link to the file:

{
  "release": {
    "label": "ctrl32 1.00.045", "version": "1.00.045",
    "size": 30117888, "sha256": "…", "filename": "ctrl32-setup-1.00.045.exe",
    "platform": "Windows", "published_at": "2026-09-11",
    "product": "ctrl32-editor",
    "archive_name": "ctrl32-1.00.045.zip", "archive_size": 29884416
  }
}

release is null when the firm has no published build of that product. The size beside a name always belongs to that same file — a page showing the archive's name with the installer's size reads right and is wrong.

The fixed addresses

These are not under /api/, because a program that was installed a year ago carries them in a manifest that cannot be edited.

Address Gives
GET /download the newest installer of the default product
GET /download/<product> that program's newest archive
GET /download/<file>.exe that exact published build
GET /update.txt?p=<product> the signed manifest, byte for byte

A product name carries no dot and a file name does, which is how /download/<segment> tells them apart. Asking /download with Accept: text/html is a browser, and it is redirected to the download page.

/update.txt also counts installations. The check carries i=<32 hex>&v=<version>&l=<language>, and one row is written per installation per product per day.

`Cache-Control: no-cache` on the manifest is load-bearing

The count is taken at the origin. A cached manifest is an update check that never arrives, and the firm's installation count quietly becomes fiction.

The customer's own doors

/api/web/v1/portal/… needs a signed-in customer and answers only about them. GET portal/downloads lists the firm's published builds, each with its product, ordered so one program's builds sit together and the newest comes first.

A download may be gated by an agreement. The door then answers 409 with agreement_required and what is missing, so the site can show the document; a POST records the acceptance and the next GET streams the file.

The machine door

POST /api/web/v1/portal/machine-login lets a firm's other server sign a customer in. It sends the shared secret in X-Machine-Secret and the customer's credentials in the body, and gets back who they are, which partners they may act for, and the licences those partners hold.

{
  "authenticated": true,
  "username": "jan@example.com", "email": "jan@example.com", "name": "Ján Novák",
  "accesses": [ … ],
  "licenses": [
    {"tier": "cloud", "key": "…", "active": true,
     "issued_at": "2026-09-11T08:00:00+00:00", "until": "2027-09-11"}
  ]
}

The key travels with the answer because the asking server verifies the signature itself against the firm's public key. active is the platform's own answer — not revoked and not run out — so the other server never has to re-derive it.

Refusal Means
403 forbidden the secret is wrong
400 refused the customer is wrong
429 throttled too many attempts, with Retry-After

Three rules this door cannot bend

The secret is checked before any credential work, so a caller who cannot prove who it is learns nothing — not even whether an e-mail address exists. No session cookie is ever set, because the other server holds its own session and a second one here would be a second truth. And it never answers 401: the operator reading the log needs 403 and 400 to wake different people.

A staff member who signs in through this door acts for nobody and gets an empty accesses list. That is an answer, not a refusal — the door does not judge who a person is.

Every machine sign-in writes a trail row with the caller's address. The secret is never part of it.

Tags
referenceapireleasesportal