---
title: Service API
summary: The REST API offers two ways to compute — batch jobs for one-off analyses and warm sessions for design loops — plus a mesh store for uploaded mesh files, case validation, progress streams, artefacts and metrics. Responses are JSON, except /metrics (Prometheus text), artefact downloads and the ?stream=true event stream; every error carries a type, a message and a hint.
section: guide
order: 3
level: 2
keywords: [REST, API, jobs, sessions, meshes, mesh store, order selection, SSE, artefacts, metrics, errors]
---
<!-- gyre docs: expanded Markdown of guide/service.md (generated tables, verification numbers and chart data are written out). Base level of this page: L2 Practice; passages at another level are bracketed by 'begin level N' / 'end level N' comments and start with [LN Name]. Built from revision ceb1f4b0be. -->

Base URL: `http://HOST:8600/api/v1`. The interactive explorer at `/api/docs` lists every
endpoint with its schema.

## Jobs: one-shot analyses

```bash
# submit
curl -s $API/jobs -H 'content-type: application/json' -d '{
  "case": "naca0012_euler",
  "overrides": {"flow.alpha_deg": 3.0},
  "gradients": {"of": ["cl", "cd"], "wrt": ["alpha", "mach"]},
  "error_estimate": ["cd"]
}'
# → 202 {"id": "job-20261110-121500-a1b2c3", "state": "queued", "device": "cpu", "digest": "…", …}

curl -s "$API/jobs/$ID?wait=60"          # long-poll until finished (or 60 s)
curl -s  $API/jobs/$ID/result            # full result record
curl -N "$API/jobs/$ID/events?stream=true"   # live Server-Sent Events
```

<!-- begin level 2 (Practice) -->
**[L2 Practice] Endpoints**

| Method and path | Purpose |
|---|---|
| `POST /jobs` | queue an analysis (`case`, `overrides`, `gradients`, `error_estimate`, `adapt`, `device`, `use_cache`, `label`) |
| `GET /jobs` | recent jobs (`?state=`, `?limit=`) |
| `GET /jobs/{id}` | state, progress, result summary (`?wait=` seconds) |
| `GET /jobs/{id}/result` | outputs, history, gradients, error estimates, surface data, provenance |
| `GET /jobs/{id}/events` | progress events (`?since=N`, `?stream=true`) |
| `GET /jobs/{id}/artifacts/{name}` | `result.json`, `solution.npz`, `flow.vtu`, `field.json`, `events.jsonl`, `error.txt` |
| `DELETE /jobs/{id}` | cancel |

<!-- end level 2 -->

<!-- begin level 2 (Practice) -->
**[L2 Practice] Order selection: `adapt`**

With `adapt` the service chooses the polynomial degree: starting at `discretization.order` it
raises the degree, warm-starting each level from the previous one, until the adjoint-based
error estimate of one output is within a tolerance.

```bash
curl -s $API/jobs -H 'content-type: application/json' -d '{
  "case": "bump_euler",
  "overrides": {"discretization.order": 1},
  "adapt": {"output": "cd", "tolerance": 1e-4, "max_order": 4}
}'
```

| `adapt` | | |
|---|---|---|
| `output` | required | scalar output whose estimated error drives the selection |
| `tolerance` | required, > 0 | absolute tolerance on that estimate |
| `max_order` | 1 to 8, default 5 | highest degree to try |

The result is that of the final degree (`spec`, `case`, `outputs`, `gradients`,
`error_estimate`, artefacts) and has an `adaptation` section:
`{output, tolerance, met, order, value, corrected, error, levels}`, where `levels` has one
record per degree tried. **Check `met`**: a job whose tolerance was not reached at `max_order`
is still `succeeded`, with the result of the highest degree that converged. The job record
carries `requested.adapt` and `result_summary.adaptation` (without `levels`). The event log
shows every degree in turn: a `case` event, the `iteration` events of its solve (each with
`order`), and one `adapt` event (the level record) that closes it; the last `case` event is
that of the result. `timing.solve` is the wall time of the whole selection and
`timing.setup` is 0.

A degree whose solve fails from the interpolated lower-degree solution is tried again before
it counts as failed: from the same guess with the full continuation (skipped when the guess is
not an admissible state), then from the free stream. Each further attempt is an `adapt_retry`
event with `order`, `reason` (why the previous attempt failed) and `start` (`interpolated` or
`free_stream`).

| Field of a level record | Meaning |
|---|---|
| `order` | polynomial degree of this level |
| `degrees_of_freedom` | size of the discretization at this degree |
| `converged` | whether the flow solve of this degree converged |
| `value` | the output at this degree |
| `estimate` | adjoint-based estimate of its discretization error |
| `corrected` | the output corrected by the estimate |
| `error` | the estimated error that is compared with `tolerance` (by absolute value) |
| `adjoint_converged` | whether the adjoint solve of the estimate converged |
| `newton_steps` | steps of the flow solve |
| `wall_time` | seconds spent on this degree, estimate included |

A degree whose flow solve did not converge has `order`, `degrees_of_freedom`,
`converged: false`, the solver's `reason` and `wall_time` only.

`met` is true only when the estimate is a finite number, its adjoint solve converged and its
absolute value is at most `tolerance`; an estimate below the tolerance from an adjoint solve
that did not converge does not end the selection. The selection also ends at the first degree
whose flow solve does not converge, below `max_order` too: `met` is false and `order`,
`value`, `corrected` and `error` are those of the last degree that converged. If the starting
degree itself does not converge, these four are `null` and the result has `converged: false`.

Unknown keys and invalid values are a 422 `ValidationError`. A selection that cannot start —
`discretization.order` is 0, or `max_order` is below `discretization.order` — is a 422
`InvalidCase`. An `output` that the case does not have is found by the worker: the job fails.

<!-- end level 2 -->

<!-- begin level 3 (Mechanics) -->
**[L3 Mechanics] States and caching**

`queued → running → succeeded | failed | cancelled`, plus `interrupted` for jobs that were active
when the service restarted. A job identical to an earlier successful one is answered immediately
from the cache with `cached_from` naming the original; pass `"use_cache": false` to force a new
run. The cache key is the specification, the gradient, error-estimate and `adapt` requests (an
omitted `max_order` equals 5), the contents of a mesh file the case names, and the code
(version, revision and a hash of the package sources): a result computed by different code, for
example before an upgrade on the same data directory, is not reused, and replacing a stored
mesh under the same name is a cache miss. On a cache hit `device` is
the device of the original run. A job that ran but did not converge is `succeeded` with
`"converged": false` — check the flag. Its outputs are those of the iterate the solve ended
at, and it has no `gradients` or `error_estimate`: the result and the job's `result_summary`
carry `skipped`, which names each requested section that is missing with the reason.
Non-finite values (a diverged solve) are reported as
`null` in job records, results, events and session replies.

<!-- end level 3 -->

## Sessions: warm solvers for design loops

```bash
SID=$(curl -s $API/sessions -H 'content-type: application/json' \
      -d '{"case": "naca0012_euler"}' | jq -r .id)

curl -s $API/sessions/$SID/evaluate -H 'content-type: application/json' \
     -d '{"inputs": {"alpha_deg": 2.5, "mach": 0.52}}'
# → {"converged": true, "nonlinear_iterations": 5, "outputs": {"cl": 0.36, …}, "wall_time": 0.8}

curl -s $API/sessions/$SID/gradients -H 'content-type: application/json' \
     -d '{"of": ["cl"], "wrt": ["alpha_deg", "x_surface"]}'

curl -s -X DELETE $API/sessions/$SID
```

<!-- begin level 2 (Practice) -->
**[L2 Practice] Endpoints**

| Method and path | Purpose |
|---|---|
| `POST /sessions` | start a solver process for a case; returns its interface (inputs, outputs, shapes) |
| `GET /sessions`, `GET /sessions/{id}` | list / describe |
| `POST /sessions/{id}/evaluate` | set inputs, converge (warm start), return outputs |
| `POST /sessions/{id}/gradients` | total derivatives at the current state |
| `POST /sessions/{id}/adjoint` | reverse total product for output seeds (coupling) |
| `POST /sessions/{id}/error-estimate` | adjoint-weighted-residual estimates |
| `GET /sessions/{id}/surface` | surface-node coordinates and current displacement |
| `GET /sessions/{id}/distributions` | wall $c_p$, $c_f$ |
| `GET /sessions/{id}/field` | field sample for visualisation |
| `GET /sessions/{id}/state` | current inputs, `solved`, `converged` and residual norm |
| `POST /sessions/{id}/tangent` | forward total product for input perturbations |
| `POST /sessions/{id}/residual` | norms of the residual of the session's state, at its inputs or at given ones |
| `POST /sessions/{id}/revert` | after an evaluation that did not converge: back to the last converged point |
| `POST /sessions/{id}/get-state`, `…/set-state` | download / upload the flow state (bytes, `application/octet-stream`) |
| `POST /sessions/{id}/checkpoint`, `…/restore` | save / restore inputs, surface displacement and state as one blob; a checkpoint also rebuilds a session that was closed or reaped (`GyreClient.restore_session`) |
| `DELETE /sessions/{id}` | close |

The last five rows go through one generic route, `POST /sessions/{id}/{command}`, which
passes any command of the solver process through (`-` and `_` are the same in the name);
`GET /sessions/{id}` lists the commands a session supports.

<!-- end level 2 -->

<!-- begin level 3 (Mechanics) -->
**[L3 Mechanics] Session behaviour**

One command runs at a time per session. The first `evaluate` compiles kernels; later ones are
warm-started and typically need a handful of Newton steps. Idle sessions close after 30 minutes;
at most four are open at once by default.

`alpha_deg` is accepted wherever `alpha` (radians) is: in `evaluate` inputs and in the `wrt` of
`/gradients`, `/adjoint` and job `gradients`. The derivative is returned under the name asked
for, per degree for `alpha_deg`. With `wrt` omitted the result has `alpha` (radians) only, and
`/state` and the session `interface` list `alpha`. Non-finite or non-numeric inputs are refused
with 422.

If an evaluation does not converge, the reply has `converged: false` with the outputs and
residual of the iterate the solve ended at, and the session keeps its last converged state as
the warm start of the next evaluation. Until an evaluation converges again, `/gradients`,
`/adjoint`, `/error-estimate`, `/distributions` and `/field` answer 422 `RuntimeError`; the
last two do so before the first evaluation as well. `converged` of `/state` is true when a
state exists and the last evaluation converged.

`case` is a preset name or a CaseSpec object, for jobs, sessions and validation alike; a string
is never read as a file path (file specifications are a CLI and Python feature). A case on a
mesh file names a mesh of the [mesh store](#meshes). A session reads that mesh once, when it
starts: it keeps running on it, and reports the `digest` of that moment, if the mesh is
replaced or deleted afterwards.

<!-- end level 3 -->

## Meshes

A case with `mesh: {generator: file, params: {path: NAME}}` refers to a mesh that was uploaded
to the service's mesh store under `NAME`. [Mesh files](meshes.md#meshes-in-a-service) has the
formats, examples for curl, Python and MCP, and the limits.

```bash
curl -s -X PUT --data-binary @wing.msh -H 'content-type: application/octet-stream' \
     $API/meshes/wing.msh
# → 201 {"name": "wing.msh", "bytes": …, "sha256": "…", "dim": 2, "elements": …,
#        "geometry_order": …, "nodes": …, "boundaries": {"wall": …, "farfield": …}, …}
```

| Method and path | Purpose |
|---|---|
| `GET /meshes` | `{"meshes": [summary, …]}`, sorted by name |
| `PUT /meshes/{name}` | store the request body (the file as it is on disk) under `name`; 201 with the summary; replaces an existing mesh of that name |
| `GET /meshes/{name}` | summary: `name`, `bytes`, `sha256`, `dim`, `elements`, `geometry_order`, `nodes`, `boundaries` (faces per group), `bounding_box`, the reader's quality numbers |
| `DELETE /meshes/{name}` | remove; `{"name": name, "deleted": true}` |

<!-- begin level 2 (Practice) -->
**[L2 Practice] Behaviour**

* A name matches `^[A-Za-z0-9][A-Za-z0-9_.-]{0,79}\.(msh|npz)$`. Anything else, including a
  name with directories, is a 422 `InvalidMeshName` on every mesh route and in
  `mesh.params.path` of a case; nothing is opened or written.
* The upload is read and validated in full. A file the reader cannot use is a 422
  `InvalidMesh` with the reader's message and is not stored; a mesh already stored under the
  name is unchanged.
* A case that names a mesh which is not stored is a 404 `UnknownMesh`, at validation, job
  submission and session creation; the hint names the `PUT` to make. A case whose
  `discretization.boundaries` does not give a condition for every boundary group of the mesh,
  or names a group the mesh does not have, is a 422 `InvalidCase`.
* An upload above `GYRE_MESH_MAX_BYTES` (64 MiB by default) is a 413 `MeshTooLarge`; a full
  store is a 507 `MeshStoreFull` ([limits](meshes.md#limits)).
* A file that was placed in the store's directory by hand and that the reader rejects is
  listed by `GET /meshes` as `{"name", "error"}` and answered 422 `InvalidMesh` by
  `GET /meshes/{name}`.

<!-- end level 2 -->

## Pipelines

| Method and path | Purpose |
|---|---|
| `GET /pipelines` | pipeline definitions available to the deployment (stages, parameters); each entry has `runnable` and, when that is false, a `reason` |
| `POST /pipelines/{name}/runs` | start a run (`params`, `force`); unchanged stages come from the cache. String `params` stay strings. Every stage's `case`, after the run's parameters are substituted, must be a preset name or a CaseSpec object (404 `UnknownPreset` otherwise, whatever the parameter is called). Runs of one pipeline are executed one after another |
| `GET /pipelines/runs`, `GET /pipelines/runs/{id}` | list runs / status; `record` is the per-stage record this run wrote, or `null` if the run ended before the engine finished |

Definitions with `shell` stages are not runnable through the service (403 `PipelineNotRunnable`)
unless it was started with `GYRE_PIPELINES_ALLOW_SHELL=1`. A run leads its own process group:
signals to the service's process group do not reach it, and at service shutdown each run is
terminated together with its stage processes.

See [Pipelines](pipelines.md) for the YAML vocabulary.

## Cases, metadata, metrics

| Method and path | Purpose |
|---|---|
| `GET /presets`, `GET /presets/{name}` | built-in cases |
| `GET /schema/case`, `GET /schema/job` | JSON Schemas |
| `POST /cases/validate` | dry run: resolved spec, size estimate, device, warnings |
| `GET /info` | version, commit, process id (`pid`), deployment name, uptime, documentation build |
| `GET /health` | liveness |
| `GET /metrics` | Prometheus text format |

## Errors

Every 4xx and 5xx answer under `/api/v1` has the same shape, with `type`, `message` and `hint`
always present:

```json
{"error": {"type": "UnknownPreset",
           "message": "unknown preset 'naca0013'",
           "hint": "available presets: ['bump_euler', 'cylinder_re40', …]"}}
```

<!-- begin level 3 (Mechanics) -->
**[L3 Mechanics] Error types**

| Status | `type` | Meaning |
|---|---|---|
| 401 | `Unauthorized` | bearer token required (`GYRE_API_TOKEN` is set) |
| 403 | `PipelineNotRunnable` | the pipeline has `shell` stages (or an invalid definition) and may not be started through the service |
| 404 | `UnknownPreset`, `UnknownPipeline`, `NotFound` | no such preset, pipeline, job, session, artefact, pipeline run or API route; `UnknownPreset` is also the answer to a pipeline run in which a stage's case is a string that is not a preset name |
| 404 | `UnknownMesh` | no mesh of that name in the mesh store: on the mesh routes, and for a case that names it |
| 405 | `MethodNotAllowed` | the path exists but not with this method; for GET, POST, PUT, PATCH and DELETE the `Allow` header lists the methods of the path (HEAD and OPTIONS are answered 405 on every API path, with an `Allow` header that is not complete) |
| 409 | `NotReady` | no result: the job has not finished, or did not succeed |
| 410 | `SessionDead` | the session process has terminated |
| 422 | `ValidationError` | the request does not match the route's schema (body, query, malformed JSON), or the case specification is invalid: mesh parameters the generator does not accept, order 0 for a viscous case, missing boundary data |
| 413 | `MeshTooLarge` | the uploaded mesh file exceeds `GYRE_MESH_MAX_BYTES` |
| 422 | `BadOverride`, `InvalidCase` | unknown override path; an empty `case`; a mesh-file case whose `discretization.boundaries` does not match the boundary groups of the mesh; an `adapt` request that cannot start |
| 422 | `InvalidMeshName`, `InvalidMesh` | a mesh name that is not a bare store name (in a URL or in `mesh.params.path`); a mesh file the reader cannot use, with the reader's message |
| 422 | `KeyError`, `ValueError`, `RuntimeError` | a session command failed: unknown input or output name, bad input value, no flow state yet |
| 429 | `TooManySessions` | session limit reached |
| 500 | `InternalError` | unexpected fault of the service. The message is `the service failed while handling this request (incident <8 hex digits>)`; the exception type and text are in the service log (logger `gyre.service`) under the same incident id and are not sent to the client |
| 500 | `SessionStartFailed` | the session process died while building the case |
| 503 | `MeshStoreInactive` | a case that names a stored mesh was sent to an application that was not started with its start-up phase (never the case under `gyre serve`); the name check (422 `InvalidMeshName`) and the store lookup (404 `UnknownMesh`) come first |
| 504 | `Timeout` | a session command, or session start-up, exceeded its time limit |
| 507 | `MeshStoreFull` | the mesh store holds `GYRE_MESH_STORE_MAX_FILES` meshes or would exceed `GYRE_MESH_STORE_MAX_BYTES` |

Session command errors carry the exception class name as `type`; classes other than the three
listed are answered with 500. An exception raised while a new session builds its case is a 422
with the class name as `type`; its hint, like that of a start-up `Timeout`, points to
`POST /cases/validate`, since there is no session to inspect. Case errors are reported by `POST /cases/validate` and, identically,
when a job or session is submitted. The `error` object inside a failed job record is
`{"type", "message"}` without a hint; the traceback of a worker exception is the `error.txt`
artefact.

<!-- end level 3 -->

<!-- begin level 3 (Mechanics) -->
**[L3 Mechanics] Python client**

`gyre.client.GyreClient` wraps the job, session, mesh, preset, validation and info endpoints
(`submit`, `job`, `wait`, `run`, `result`, `events`, `artifact`, `cancel`, `session`, `meshes`,
`upload_mesh`, `mesh`, `delete_mesh`, `presets`, `validate`, `info`) and raises `GyreError`
carrying the status and the structured error. `submit(..., adapt={"output": "cd",
"tolerance": 1e-4})` and `run(..., adapt=...)` send an order selection;
`upload_mesh(path, name=None)` stores a local file under its own name unless `name` is given. Pipelines, metrics and
schemas are reached with plain HTTP. See [MDO integration](mdo-integration.md) for
`RemoteDiscipline`.

<!-- end level 3 -->
