---
title: "Check a template document"
method: POST
path: "/templates/validate"
tags: ["Templates"]
---

# Check a template document

`POST /templates/validate`

## Request

- Base URL: `https://app.mandala.computer/api/v1`
- URL: `https://app.mandala.computer/api/v1/templates/validate`
- Auth: HTTP bearer

Check a template document against the schema and the rules the publish path applies, without publishing anything. Nothing is stored and no ref is claimed, so this is safe to call on a draft and safe to call repeatedly.

The body is the document itself, as JSON or YAML — not a JSON wrapper around it. Send `Content-Type: application/yaml` or `application/json`; either is read.

A document that is wrong is reported with **every** problem one pass can reach rather than the first, so a file with four mistakes takes one call — see `problems` in the response for the one case where fixing a problem reveals another. On success the two digests come back: `doc_digest` identifies the document and changes with anything that changes what it MEANS, and `build_digest` covers only what decides the image — so a new label or a version bump leaves it alone. `build_digest` is present only for a document with no `spec.from`, and such a document builds nothing: `spec.build` and `spec.env` need a parent. A document that names a parent in `spec.from` — which is every document that builds — has no `build_digest`, because that cannot be computed without the parent image’s, so comparing it cannot tell you whether an edit to such a document means a rebuild.

Requires the `member` role or higher.

## Response `200`

`valid` is what you branch on, and both branches are 200: an invalid document is an answer to the question, not a failed request. `false` brings `problems` and nothing else; `true` brings the ref, the digests, the catalogue row and the canonical bytes.

Three of the fields below — `template`, `canonical` and `build_digest_needs` — were on this wire and in no reader’s copy of it until OPL-4179, which wrote them out here in prose. OPL-4190 gave the response a schema, so the table is now held against the code that produces it rather than kept in step by hand. In the same change `template` stopped carrying the on-host image `family`, which is ours rather than yours and was already projected away on every other route that answers a catalogue row.

- TemplateValidation
  - `valid` boolean, required: Whether the DOCUMENT is well formed — the schema and the rules that read the file itself. It is not a publish preflight: a valid document is still refused by `POST /templates` for things only your account decides, and none of them are visible here — a namespace that is not yours, a plan that does not carry publishing, a ref already taken, the per-account ceiling, an `os` that contradicts the `family` the document names, a `spec.hardware.disk_gb` below the disk floor of the family or of the `spec.from` parent (400), and a document with `spec.build` steps or `spec.env` naming a family that is not yours (403).
  - `problems` string[]: Present when `valid` is false. A document that could not be PARSED has exactly one entry, because nothing after the syntax error was read. A document that parsed is reported with every problem one pass can reach rather than the first, so a file with four mistakes in four fields takes one call — with one exception worth knowing: a build step that is structurally wrong (it does nothing, or carries more than one operation) is reported as that, and the problems INSIDE it are not looked at until you have fixed it. Each names what is wrong in the author’s own vocabulary.
  - `ref` string: `namespace/name@version`, as the document’s metadata spells it.
  - `doc_digest` string: Identifies the document, as `sha256:…`. Taken over `canonical` below — so it changes with anything that changes what the document MEANS, a label included, and NOT with comments, key order, indentation or YAML-versus-JSON, none of which survive parsing. Being over `canonical` is also what lets you check it yourself.
  - `template` Template
    - `name` string: The short name of the template, and what a computer reports as its own `template`. Accepted as `template` on a create for the templates we publish; one you published is named by its `ref`.
    - `ref` string: The pinned `namespace/name@version` for this template. Also accepted as `template` on a create, and the form to use when it matters that you get exactly this template and this version — a short name resolves to whatever the host currently has under it.
    - `label` string: Human-readable name.
    - `os` string: `linux`. (`windows` is not currently offered on any plan.)
    - `desktop` string: The display protocol this template’s desktop speaks: `wayland`, `x11`, or absent (X11, or a host too old to say); branch on `=== "wayland"`. Screenshots, input and `exec` are identical either way — they are taken below the guest — but two things differ on a Wayland template. A window `id` is the compositor’s address rather than an X window id (opaque and handed back the same way, but not comparable across the two), and moving or resizing a window the compositor is TILING is refused rather than silently ignored, because a tiled window’s geometry belongs to the layout. Absent also on a host deployed before this field existed, which is why it is not defaulted to `x11` here.
    - `icon` string: A slug naming a mark this console has an asset for, e.g. `claude` or `debian`. Cosmetic — nothing here validates it against a list, and an unrecognised value or none just means the template has no particular icon.
    - `cpu` integer: The default this template builds at.
    - `ram_mb` integer
    - `disk_gb` integer: The FLOOR this template builds at, not a default like the two above — a create asking for less is raised to this and charged at it.
  - `canonical` string: The document as the digests were taken over it, with key order and whitespace normalised. Two files differing only in comments and key order are the same document and hash the same.
  - `build_digest` string: Covers only what decides the image, as `sha256:…` — so a new label or a version bump leaves it alone. Present only for a document with no `spec.from`, which builds nothing (`spec.build` and `spec.env` need a parent), so it cannot tell you whether an edit to a document that builds means a rebuild. Replaced by `build_digest_needs` on a document with a parent.
  - `build_digest_needs` string: Replaces `build_digest` on a document that names a parent in `spec.from`: a sentence saying what could not be computed. A build digest needs an identity for the CONTENTS of the base image, which is a fact about a host holding it rather than anything in your document.

## Other responses

- `400`: The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.
- `401`: Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).
- `403`: The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: "revoked"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.
- `405`: This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.
- `413`: What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.
- `429`: You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: "contention"` (see `POST /snapshots/{id}/clone`).
- `500`: The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.
- `503`: Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.

---

[API](https://skmtc.dev/mandala/apis/mandala-computer-api.md) · [All operations](https://skmtc.dev/mandala/apis/mandala-computer-api/llms.txt) · [OpenAPI document](https://skmtc.dev/mandala/apis/mandala-computer-api/revisions/2afbb87fa703?raw)
