HTTP API¶
automathd exposes a small HTTP API. Most endpoints call the automath
render/check services in-process — the same code the automath CLI runs,
invoked directly (no per-request subprocess) and their result streamed back;
the font-management endpoints (/fonts, /fonts/storage, /fonts/download)
are served directly from the daemon's font store.
Common behavior¶
CORS. All responses include CORS headers
(Access-Control-Allow-Origin: *, Access-Control-Allow-Methods: GET, POST,
OPTIONS, Access-Control-Allow-Headers: Content-Type, Authorization). An
OPTIONS request to any path returns 204 No Content (preflight).
Rate limiting. Requests are throttled per client IP with a sliding window,
bucketed by endpoint family (render, check, upload) so a burst of cheap
validation calls and a burst of expensive renders are limited independently —
see Configuration. Exceeding a bucket returns 429 Too
Many Requests.
The client IP is taken from the reverse proxy — never from the request
itself (an earlier ip query parameter drove this and has been removed; it was
both spoofable and, worse, a SQL-injection vector). When REMOTE_ADDR is a
configured trusted proxy, the address is read from X-Real-IP (then the first
X-Forwarded-For hop); a direct client is limited by its REMOTE_ADDR, and a
forwarding header it sends itself is ignored. If a trusted proxy forwards no
client-IP header, the request is left unlimited rather than counted against the
proxy's own address (which would throttle every visitor at once) — so deploying
behind nginx requires proxy_set_header X-Real-IP $remote_addr;.
Font selection. The rendering endpoints — /render/html, /render/svg,
/render/wrappers and /render/templates — accept an optional set of font
query parameters, all resolved server-side against the daemon's font store.
/render/atmxs does not bake fonts (its snapshot holds only display
strings), so it ignores all of these except font_mode, which it stamps into
the snapshot for a later render to reuse (see below):
| Parameter | Purpose |
|---|---|
text_font, math_font, text_bold_font |
ids of stored fonts to use |
text_font_name, math_font_name |
family-name fallbacks, used when an id does not resolve on this instance (e.g. a private upload from another daemon) |
font_mode |
embedded | served | local — how fonts are delivered in produced HTML. Honored by /render/html and /render/wrappers; on /render/atmxs it is stamped into the snapshot (a top-level font_mode entry) so a later /render/html or CLI render reuses it unless overridden. Forced to local whenever a selected font is a private upload (its licence is unverified, so its bytes are never embedded or served) |
Fonts can only be referenced by id (resolved against the store); the raw
fonts JSON override that the automath --fonts CLI accepts is intentionally
not exposed over HTTP, since it could name arbitrary server file paths that the
renderer would read and embed.
When no font parameter is given, the daemon's configured fonts are used.
Error codes.
| Code | Meaning |
|---|---|
400 Bad Request |
missing or malformed input (parameter or body) |
401 Unauthorized |
missing or invalid publish token (/whoami, /publish) |
404 Not Found |
unknown path, or unknown doc ref |
405 Method Not Allowed |
wrong HTTP method for the path |
429 Too Many Requests |
rate limit hit (see above) |
500 Internal Server Error |
the underlying automath call failed |
503 Service Unavailable |
publishing is not configured on this server (/whoami, /publish) |
/render/html¶
Produce an HTML page. Endpoints are named after their output format; the
source is conveyed by the HTTP method and parameters, mirroring the
polymorphic <source> of automath render html.
GET /render/html¶
Render a registered document as an HTML worksheet — static by default
(questions and answers, print button only), or interactive with
interactive=1 (input fields and answer checking).
| Parameter | Required | Values | Default |
|---|---|---|---|
ref |
yes | a valid doc ref (e.g. 6e/cm1_blanche::multiplications) |
— |
interactive |
no | 1 for the live, input-checking page |
static |
locale |
no | display locale | configured locale |
sl_locale |
no | school-level locale (content path); defaults to locale |
— |
If ref is not in the registered docs for the resolved locale, the server
replies 404.
GET http://127.0.0.1:9998/render/html?ref=6e/cm1_blanche::multiplications
GET http://127.0.0.1:9998/render/html?ref=6e/cm1_blanche::multiplications&interactive=1
Returns text/html.
POST /render/html (inline script)¶
Render a document whose YAML is posted verbatim — used by the workspace
Library, whose entries are not registered in the content index. The body is
written to a temporary file and rendered with
automath render html <tmpfile>::<script_name>.
| Parameter | Required | Values | Default |
|---|---|---|---|
script_name |
yes | name of the script inside the posted YAML | — |
interactive |
no | 1 for the live, input-checking page |
static |
locale |
no | display locale | configured locale |
sl_locale |
no | school-level locale; defaults to locale |
— |
POST http://127.0.0.1:9998/render/html?script_name=multiplications
Content-Type: text/plain
multiplications:
- preset: "grid"
...
Returns text/html.
POST /render/html (single ATMXS document)¶
Render a single ATMXS document as a standalone HTML page (questions and answers,
print button only — no interactive form). This is the default POST mode —
no script_name and no answers parameter. A snapshot carries no
answer-validation data, so interactive=1 is rejected here with 400; render
interactively from the script source instead (POST ?script_name=...).
| Parameter | Required | Values | Default |
|---|---|---|---|
locale |
no | display locale | configured locale |
The request body must contain exactly one ATMXS YAML document.
POST http://127.0.0.1:9998/render/html
Content-Type: text/plain
preset: grid
title: "Multiplications"
questions:
- q: 4 × 6 = Q{}
a: 24
Returns text/html.
POST /render/html?answers=1¶
Render a combined answers-only HTML page from one or more ATMXS documents.
| Parameter | Required | Values | Default |
|---|---|---|---|
answers |
yes | 1 |
— |
locale |
no | display locale | configured locale |
The request body must contain one or more ATMXS YAML documents separated by
---. Each document produces one answers section (or two if it contains a
valid rotate entry). Sections are separated by print page breaks.
POST http://127.0.0.1:9998/render/html?answers=1
Content-Type: text/plain
preset: grid
title: "Multiplications"
questions:
- q: 4 × 6 = Q{}
a: 24
---
preset: grid
title: "Additions"
questions:
- q: 7 + 3 = Q{}
a: 10
Returns text/html.
/render/atmxs¶
Produce an .atmxs intermediate document from a script source.
GET /render/atmxs¶
| Parameter | Required | Values | Default |
|---|---|---|---|
ref |
yes | a valid doc ref (e.g. 6e/cm1_blanche::multiplications) |
— |
rotate |
no | 1 to inject a random rotate entry |
0 |
font_mode |
no | embedded | served | local — stamped into the snapshot for a later render to reuse |
not stamped |
locale |
no | display locale | configured locale |
sl_locale |
no | school-level locale; defaults to locale |
— |
GET http://127.0.0.1:9998/render/atmxs?ref=6e/cm1_blanche::multiplications
GET http://127.0.0.1:9998/render/atmxs?ref=6e/cm1_blanche::multiplications&rotate=1
Returns text/plain.
POST /render/atmxs (inline script)¶
Render a document whose YAML is posted verbatim (workspace Library) to
.atmxs, with automath render atmxs <tmpfile>::<script_name>.
| Parameter | Required | Values | Default |
|---|---|---|---|
script_name |
yes | name of the script inside the posted YAML | — |
rotate |
no | 1 to inject a random rotate entry |
0 |
font_mode |
no | embedded | served | local — stamped into the snapshot for a later render to reuse |
not stamped |
locale |
no | display locale | configured locale |
sl_locale |
no | school-level locale; defaults to locale |
— |
POST http://127.0.0.1:9998/render/atmxs?script_name=multiplications
Content-Type: text/plain
multiplications:
- preset: "grid"
...
Returns text/plain.
GET /render/svg¶
Render a single atmxg expression as a standalone SVG (wraps
automath render svg). Used, for example, by automathon's live font preview.
| Parameter | Required | Values | Default |
|---|---|---|---|
expr |
yes | an atmxg expression string | — |
locale |
no | locale (affects number formatting) | configured locale |
The selected fonts (see Font selection) drive both the
glyph metrics (computed server-side from the font files) and the font-family
names referenced in the SVG. There is no font_mode: a standalone SVG
references fonts by name only — the caller is responsible for loading the
matching @font-face.
Returns image/svg+xml.
GET /render/wrappers¶
Return the mail-merge HTML wrapper parts as JSON. No body needed.
| Parameter | Required | Values | Default |
|---|---|---|---|
locale |
no | display locale | configured locale |
Returns application/json.
POST /render/templates¶
Render a single ATMXS document as a JSON template (via
automath render templates), for mail-merge assembly.
| Parameter | Required | Values | Default |
|---|---|---|---|
locale |
no | display locale | configured locale |
The request body must contain exactly one ATMXS YAML document.
POST http://127.0.0.1:9998/render/templates
Content-Type: text/plain
preset: grid
title: "Multiplications"
questions:
- q: 4 × 6 = Q{}
a: 24
Returns application/json.
POST /check/qspec¶
Validate a single question-spec line (wraps automath check qspec). See
Validating specs.
The request body is the plain-text spec (UTF-8).
Returns application/json, always with status 200:
or, for an invalid spec:
A spec may be valid yet questionable — for instance a constraint that leaves
only a couple of drawable values. Such remarks come back in an optional
warnings array, present only when there is something to report:
POST /check/qblock/batch¶
Validate several full questions: blocks in a single call (wraps automath
check qblock --batch). See Validating specs.
The request body is a JSON array of {name, text} objects (one per block).
POST http://127.0.0.1:9998/check/qblock/batch
Content-Type: application/json
[
{"name": "series_a", "text": "questions:\n - (5) multi_direct 2-9×2-9\n"},
{"name": "series_b", "text": "questions:\n - (5) add_direct 2-9+2-9\n"}
]
Returns application/json, in input order:
[
{"name": "series_a", "errors": []},
{"name": "series_b", "errors": [{"line": 2, "message": "..."}]}
]
The line field is 1-based within the block, so the editor can map each error
back to the right line of the YAML.
A block that is valid yet questionable also carries a warnings array of the
same shape, present only when there is something to report:
Warnings never make a block invalid — a client that ignores the key behaves exactly as before.
POST /check/document¶
Validate a whole script document: the YAML of a script file, with the one or more top-level series it defines. This is the endpoint an editor calls to have automath itself judge a document — its structure as well as its questions — instead of reimplementing the rules client-side.
The request body is the raw YAML. The optional preset query parameter
(grid, slideshow) states which preset every series must declare; an
unknown value is a 400.
POST http://127.0.0.1:9998/check/document?preset=grid
Content-Type: text/plain; charset=utf-8
multiplications:
- preset: "grid"
- title: "Mental calculation"
- exercise:
- questions: |
(10) multi_direct 2-9×2-9
Returns application/json:
{
"errors": [],
"series": [
{"name": "multiplications",
"errors": [{"line": 7, "message": "..."}],
"warnings": [{"line": 6, "message": "..."}]}
]
}
- the top-level
errorsare the file-level ones — unparseable YAML, a document that is not a mapping of series name to body. When it is non-empty,seriesis empty: nothing else could be looked at. - each entry of
seriesreports one top-level key, in document order. - every
lineis 1-based within the submitted document (not within the questions block), so the editor highlights it directly;0means the problem has no single line to point at. Comment lines count as lines, so post the document verbatim and the numbers land where its author is looking.
What is checked, per series: the body is a list of sections, each carrying a
single key; preset is present, registered, and the one the caller expects;
the preset's expected number of exercise sections is there; each exercise
carries a questions block; and that block goes through the full
qblock validation.
What is not checked here: anything that belongs to the client's own data
model rather than to automath — for instance which series names are legal, or
whether a document must carry a title. Those stay with the client.
The check builds nothing: the document classes are looked up, never instantiated. No question is drawn, no source is opened and no alternator state moves — none of what a render does.
Font management¶
These endpoints are served straight from the daemon's font store (they do not
shell out to automath). They back the font settings in automathon.
A private upload is a font a client has uploaded for its own use: its licence is unverified, so the daemon never lists it back to other clients nor redistributes its bytes. A catalogue font is an operator-curated, licence-cleared family the daemon may embed, serve or hand out for local installation.
POST /fonts¶
Currently disabled
Font uploads are switched off: every POST /fonts answers 403 Forbidden
with {"error": "Font uploads are disabled on this server."}, and the
matching button in automathon is greyed out. The endpoint is
unauthenticated and the uploads directory is bounded by an LRU cap
(fonts.max_upload_bytes_total), so anyone reaching the daemon could evict
everybody else's stored fonts. The feature will come back once uploads are
authenticated and capped per client. The description below documents the
behaviour it will have then (and the code kept for it).
Store an uploaded font (the request body is the raw font bytes) and return its
metadata. Stored bytes are deduplicated by content hash; the returned id is
what the client keeps in its own workspace to select the font later. There is
deliberately no GET (global list) nor DELETE — a private upload is never
redistributed by the host.
| Parameter | Required | Values | Default |
|---|---|---|---|
filename |
no | original file name (used to infer the format/family) | — |
POST http://127.0.0.1:9998/fonts?filename=MyFont.woff2
Content-Type: application/octet-stream
<raw font bytes>
On success returns 201 Created with application/json (the font metadata,
including its id). A rejected font (bad format, etc.) returns 400 with
{"error": "..."}.
GET /fonts/storage¶
Return the distributable font catalogue — the licence-cleared families the daemon may embed or serve — as JSON, grouped by family. No parameter needed.
Returns application/json.
GET /fonts/download¶
Download a catalogue family as a .zip of OS-installable faces (ttf/otf), so a
teacher can install it locally and pick the local delivery mode. Catalogue
only — private uploads are never served out.
| Parameter | Required | Values | Default |
|---|---|---|---|
id |
yes | a catalogue font id | — |
Returns application/zip (Content-Disposition: attachment). An unknown id
returns 404.
Publishing¶
The Publication panel's Publish button ships a self-contained web-app
bundle to the daemon, which unpacks it under a static serve root so it is served
at automathics.net/<identifier>. Authentication is a bearer token (no
account system yet): a pre-shared secret whose sha256 is matched against the
configured publish.tokens table to resolve an identifier. The identifier is
decided server-side from the token — a client can never claim one it lacks the
token for.
Publishing is opt-in: with no serve_root configured (see
Configuration) the daemon is not a publishing host and
both endpoints answer 503.
Both endpoints read the token from an Authorization: Bearer <token> header.
GET /whoami¶
The publisher-session check (behind the panel's Se connecter / Log in button, and the on-load re-validation). Resolves the token to its identifier.
Returns 200 with application/json {"identifier": "<id>"} on a valid token,
401 on a missing/invalid one, 503 when publishing is disabled.
POST /publish¶
Publish a web app. The request body is a zip of the bundle — index.html
plus an optional assets/ folder (only those entries are accepted; anything
else, or a path escaping via .., is rejected). The bundle is unpacked
atomically to <serve_root>/<identifier>/.
POST http://127.0.0.1:9998/publish
Authorization: Bearer <token>
Content-Type: application/zip
<zip bytes: index.html [+ assets/…]>
Returns 200 with application/json
{"identifier": "<id>", "url": "<public url>"} on success. 401 on a
missing/invalid token, 503 when publishing is disabled, 400 on an empty body
or a malformed bundle ({"error": "..."}).