Skip to content

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.

GET http://127.0.0.1:9998/render/svg?expr=1/4 + 3/8 = ?

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
GET http://127.0.0.1:9998/render/wrappers

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).

POST http://127.0.0.1:9998/check/qspec
Content-Type: text/plain

multi_direct 2-9×2-9

Returns application/json, always with status 200:

{"valid": true}

or, for an invalid spec:

{"valid": false, "error": "<error message>"}

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:

{"valid": true, "warnings": ["<warning message>", "..."]}

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:

[
  {"name": "series_a", "errors": [],
   "warnings": [{"line": 1, "message": "<warning message>"}]}
]

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 errors are the file-level ones — unparseable YAML, a document that is not a mapping of series name to body. When it is non-empty, series is empty: nothing else could be looked at.
  • each entry of series reports one top-level key, in document order.
  • every line is 1-based within the submitted document (not within the questions block), so the editor highlights it directly; 0 means 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.

GET http://127.0.0.1:9998/fonts/storage

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
GET http://127.0.0.1:9998/fonts/download?id=<catalogue-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.

GET http://127.0.0.1:9998/whoami
Authorization: Bearer <token>

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": "..."}).