# Simien Central API — Guide

Run SIMIEN building-energy simulations over HTTP, and change a building's numbers (u-values, schedules, prices…)
and re-simulate it, without the SIMIEN desktop software.

The API is built around **`.simx`**, SIMIEN's encrypted building file. Because a `.simx` can't be opened and edited
by hand, you change it through **simulatable parameters**: the API lists every number you may change, you send back
the numbers you want different, and the edit is applied inside the service for that one run. You never need to
decrypt the file to work with it, and we never store it. Plaintext `.sxi` files work everywhere too (§2.2).

**The easiest way to start:** give this link — `https://central-api.simien.no/guide.md` — to an AI
assistant or coding agent that can run code or make HTTP calls, together with your `.simx` and what you want to find
out. It can work out the rest from this guide, including the rules in §12. Keep your API key private: only give it
to tools you trust.

- **Base URL:** `https://central-api.simien.no` — every API path starts with `/api/v1/sim/`
- **Authentication:** `Authorization: Bearer <api-key>` on every call (§2.1)
- **Interactive reference:** `/docs` (Swagger) · **this guide:** `/` (HTML) and `/guide.md` (Markdown)
- **For LLM agents and code generators:** `/llms.txt` · `/openapi.json` ·
  `/schemas/simulatable-parameters.schema.json`
- **Get an API key:** https://simien.no/kontakt-oss/
- **License user terms:** https://simien.no/wp-content/uploads/Bruksvilkar-Simien-2026.pdf ·
  **Privacy policy:** https://simien.no/personvernerklaering/

---

## 1. Quick start: five calls

This is the whole workflow on a real building (a 16-zone house). Each step uses a value from the step before.

```bash
API=https://central-api.simien.no/api/v1/sim
AUTH="Authorization: Bearer $API_KEY"

# 1. What can I change, and what can I run?
curl -s -X POST "$API/simulatable-parameters" -H "$AUTH" -F "file=@building.simx" -o parameters.json
```

From `parameters.json` take three things:

```jsonc
{
  "source": {
    "sha256": "29b6901aa58674bb…",      // → identifies this building; needed in step 3
    "simien_version": "8.1.1.15"        // → use as simien_version
  },
  "simulations": [
    {"id": "winter-simulation#159", "type": "wintersim", "name": "Winter Simulation 1"},
    {"id": "year-simulation#158",   "type": "yearsim",   "name": "Year Simulation 1"},   // → use as params
    {"id": "profit-evaluation#230", "type": "profitsim", "name": "Profit Evaluation 1"}
  ],
  "parameters": { … {"id": "facade#178", "label": "Facade 'living_room_and_kitchen_89bc1d78..Face2_ExteriorWall'",
                     "values": {"uvalue": 0.18, "thermal_cap": 2.4, "ext_absorption_coeff": 0.8}} … }
}
```

```bash
# 2. Run the simulation as it is (the baseline)
curl -s -X POST "$API/simulate" -H "$AUTH" -F "file=@building.simx" \
  -F "params=year-simulation#158" -F "simien_version=8.1.1.15" -F "format=catalog" -o baseline.json

# 3. Write the change you want: facade#178 gets a better wall (use the full sha256)
cat > edit.json <<'EOF'
{"source": {"sha256": "29b6901aa58674bb…"},
 "parameters": [{"id": "facade#178", "values": {"uvalue": 0.10}}]}
EOF

# 4. Run it again with the change (the same .simx + the edit)
curl -s -X POST "$API/simulate" -H "$AUTH" -F "file=@building.simx" \
  -F "simulatable_parameters=@edit.json;type=application/json" \
  -F "params=year-simulation#158" -F "simien_version=8.1.1.15" -F "format=catalog" \
  -D headers.txt -o edited.json

# 5. Confirm the change was applied, then compare baseline.json with edited.json (§6.3)
grep -i x-simulatable-parameters-changed headers.txt      # → 1
```

`edited.json` contains `"applied_changes": {"changed": 1, "applied": [{"address": "facade#178", "attribute":
"uvalue", "old": 0.18, "new": 0.1, "label": "Facade 'living_room_and_kitchen_89bc1d78..Face2_ExteriorWall'",
"active": true}]}` next to the results. That one wall changed 160 of 610 result variables and cut room heating from
7,038 to 6,912 kWh/year. The rest of this guide explains each step in detail.

---

## 2. Basics

### 2.1 Authentication

```
Authorization: Bearer sk_live_<identifier>_<secret>
```

Every endpoint needs the key except the health checks (`/healthz`, `/api/v1/sim/health`). Keep the key on your
server; never ship it in a browser app. A missing key returns `401 MISSING_API_KEY`, a wrong one
`401 INVALID_API_KEY`.

### 2.2 Uploading files

- Requests with files are `multipart/form-data`.
- The building always goes in a field named **`file`**: your `.simx`. It is decrypted in memory for that one
  request and never stored.
- **`.sxi` works too.** A plaintext `.sxi` is accepted everywhere a `.simx` is, with the same results — except
  `simx-metadata`, which takes `.simx` only.
- The `sxo/*` endpoints take a **result** (`.sxo`) in the same `file` field, not a building.
- Files are limited to **50 MB**; a larger one gets `413 FILE_TOO_LARGE`.
- A simulatable-parameters document goes in a **second file field** named `simulatable_parameters`, not a text field.

### 2.3 How long calls take

Measured on a 10-zone office building and a 16-zone house:

| Call | Typical time |
|---|---|
| `simulatable-parameters`, `simx-metadata`, `versions`, `locations` | under 1 s |
| `sxo/catalog`, `sxo/info` on a 10 MB result | 1–5 s |
| `sxo/parse` on a 10 MB result | 5–40 s (the same work as `format=zip`) |
| `simulate`, `format=sxo` | 1–5 s |
| `simulate`, `format=catalog` | 2–15 s |
| `simulate`, `format=zip` | 5–40 s |
| `simulate-all` | a few seconds per simulation (they run in parallel) |

Use a client timeout of **at least 120 s** for `simulate` and more for `simulate-all` on large buildings.
Results can be large: a year simulation's `.sxo` is around 10 MB, its catalog around 11 MB.

### 2.4 Rate limits

Limits are per endpoint and per client IP. Every rate-limited response carries `X-RateLimit-Limit`,
`X-RateLimit-Remaining` and `X-RateLimit-Reset` (Unix time). When you exceed a limit you get
`429 RATE_LIMIT_EXCEEDED` with a `Retry-After` header and `retry_after` (seconds) in the body. Other endpoints are
unaffected.

### 2.5 Errors in one paragraph

Every error is `{"error": {"code": "…", "message": …, "request_id": "…", "doc_url": "…"}}`. **Branch on `code`**, not on the text.
`message` is a string, or — when the simulation service explains an input problem — an object with `code`,
`message`, `job_id`, and often `problems` (a list) and `hint`. Quote `request_id` (also the `X-Request-Id` header)
and `X-Job-ID` when you contact support. `doc_url` links to the code's explanation in the guide. Full list: §11.

---

## 3. Endpoints

| Method | Path | What it does | Limit/min |
|---|---|---|---|
| POST | `/api/v1/sim/simulatable-parameters` | Every number you may change in a building, plus the simulation ids and version | 120 |
| POST | `/api/v1/sim/simulate` | Run **one** simulation; result as `.sxo`, JSON catalog or ZIP | 120 |
| POST | `/api/v1/sim/simulate-all` | Run **every** simulation in the file; ZIP with one result per simulation | 20 |
| POST | `/api/v1/sim/simx-metadata` | Describe a building: zones, areas, surfaces, systems | 120 |
| POST | `/api/v1/sim/sxo/catalog` | Variable catalog of a result (`.sxo`) you already have | 120 |
| POST | `/api/v1/sim/sxo/info` | What a result (`.sxo`) contains: metadata, zones, tables, graphs | 120 |
| POST | `/api/v1/sim/sxo/parse` | A result (`.sxo`) exported as a ZIP of JSON and CSV | 120 |
| GET | `/api/v1/sim/versions` | SIMIEN versions you can pass as `simien_version` | 120 |
| GET | `/api/v1/sim/locations` | Climate locations you can pass as `location` | 120 |
| GET | `/api/v1/sim/health` | Service status and versions (no key) | – |
| GET | `/healthz` | Gateway status (no key) | – |


**Which one do I want?**

| I want to… | Use |
|---|---|
| Run a building as it is | `simulatable-parameters` (for the id and version), then `simulate` |
| Run every simulation in the file at once | `simulate-all` |
| Change numbers and see the effect | `simulatable-parameters` → edit → `simulate` with `simulatable_parameters` (§9) |
| Show a person what the building contains | `simx-metadata` |
| Turn a `.sxo` I already have into numbers | `sxo/catalog` (or `sxo/parse` for CSV) |

`simx-metadata` and `simulatable-parameters` read the same `.simx` but answer different questions:

|  | `simx-metadata` | `simulatable-parameters` |
|---|---|---|
| Question | *What is this building?* | *What can I change?* |
| Content | Named zones with area and volume, surfaces, windows, systems | Every editable number, addressed by `id`, with limits |
| Can be sent back to `simulate` | No | **Yes** |
| Also takes `.sxi` | No | Yes |

---

## 4. Choosing the inputs

### 4.1 Simulation id (`params`)

Read it from `simulations[].id` in the `simulatable-parameters` response — ids differ per file
(`year-simulation#158`, `aarsim#23`, …) and cannot be guessed. The list contains every id `simulate` accepts:
simulations (year, winter, …) and evaluations such as the energy label (`energimerke26#545`) and profitability
(`profit-evaluation#230`). Send exactly one id. `simulate-all` runs all the simulations in one call, but not the
evaluations — run those one at a time with `simulate`.

### 4.2 Version (`simien_version`)

The simplest reliable choice is `source.simien_version` from `simulatable-parameters` — the version the building
was saved with. Any version from `GET /api/v1/sim/versions` also works (newest first). Both spellings are
accepted: `8_1_2_22` and `8.1.2.22`. SIMIEN 7 is not supported. `simulate-all` reads the version from the file if
you leave it out.

### 4.3 Location (`location`)

Optional. Leave it out to keep the climate stored in the file — the `X-Climate` response header tells you which
climate was used. To override, pass a name exactly as returned by `GET /api/v1/sim/locations`, including
æ/ø/å (`Bodø`, `Tromsø`). An unknown name returns `400 SIMULATION_REJECTED` ("Unknown location …").

---

## 5. Run a simulation — `POST /api/v1/sim/simulate`

| Field | Required | Description |
|---|---|---|
| `file` | yes | Your `.simx` (§2.2) |
| `params` | yes | One simulation id (§4.1) |
| `simien_version` | yes | §4.2 |
| `location` | no | §4.3 |
| `format` | no | `sxo` (default), `catalog` or `zip` — see below |
| `simulatable_parameters` | no | Your edits, as a JSON file (§9) |

| `format` | You get | Size (year simulation) |
|---|---|---|
| `sxo` | The raw SIMIEN result file (XML), `application/octet-stream` | ~10 MB |
| `catalog` | One JSON document with every result variable (§6), `application/json` | ~11 MB |
| `zip` | The `.sxo` plus parsed exports: `metadata.json`, `graphs/*.json`, `zones/*.json`, …; `_applied_changes.json` when you sent edits | ~5 MB, 200+ files |

The file name (`Content-Disposition`) is `{file}_{version}_{simulation}_{climate}_{timestamp}` plus `.sxo`,
`_catalog.json` or `_export.zip`; the `.sxo` inside the ZIP has the same name as the `sxo` download. Names with
æ/ø/å are sent as an ASCII `filename` plus a UTF-8 `filename*`.

| Response header | Meaning |
|---|---|
| `X-Job-ID` | Id of this run — quote it to support |
| `X-Simien-Version` | Version that was run |
| `X-Climate` | Climate used (from the file, or your `location`). Non-ASCII names are percent-encoded: `Troms%C3%B8` = Tromsø |
| `X-Simulatable-Parameters-Changed` | How many values your edits changed (only when you sent `simulatable_parameters`) |

---

## 6. Reading the results

### 6.1 The catalog

`format=catalog` returns:

```jsonc
{
  "metadata": {"project": "…", "sim_type": "year_sim", "total_zones": 10, "total_variables": 610, "generated_at": "…", "sxo_file": "…"},
  "variables": [
    {
      "variable_name": "Energibehov",           // what it is (Norwegian, as in SIMIEN)
      "heading": "Netto energibehov (A)",       // the table/graph it comes from
      "zone": "Alle soner",                     // "Alle soner" = whole building, or a zone name
      "source_type": "table", "source_id": "table#1",
      "data_type": "time_series", "unit": "kWh",
      "labels": ["1a Romoppvarming", "1b Ventilasjonsvarme (varmebatterier)", "2  Varmtvann (tappevann)", …,
                 "Totalt netto energibehov, sum 1-6"],
      "values": [7038, 400, 6504, …, 23207],
      "value": null,                            // used instead of values for single numbers
      "climate": "Oslo", "sim_type": "year_sim", "sxo_file": "…", "column_index": 1
    },
    …
  ],
  "applied_changes": { … }                      // only when you sent simulatable_parameters (§9.5)
}
```

`labels` and `values` are parallel arrays. The net energy demand of the whole building, for example, is the
variable with `variable_name: "Energibehov"` and `zone: "Alle soner"`; its label
`"Totalt netto energibehov, sum 1-6"` holds the total in kWh/year.

### 6.2 The ZIP

`format=zip` contains the `.sxo`, `metadata.json`, and one JSON file per graph (`graphs/graph_<n>.json`) and per
zone (`zones/zone_<n>_<name>.json`), plus `_applied_changes.json` when you sent edits.

### 6.2a A result you already have — `POST /api/v1/sim/sxo/…`

The three `sxo` endpoints read a `.sxo` you already have (from `format=sxo`, `simulate-all` with `format=sxo`,
or the SIMIEN desktop software) without simulating anything. Send it in the field `file`; it must end in `.sxo`.

| Endpoint | Returns | Same as |
|---|---|---|
| `sxo/catalog` | `{name}_catalog.json`: the catalog in §6.1 | `simulate` with `format=catalog` |
| `sxo/parse` | A ZIP of JSON files and CSV files of every table, graph, bar chart and the hourly values, plus the catalog | `simulate` with `format=zip` |
| `sxo/info` | A JSON file describing the result: project, version, simulation type, zones with element counts, the lists of tables and graphs, error and warning counts | – |

A file that isn't a SIMIEN result gets `400 SXO_UNREADABLE`.

### 6.3 Comparing two runs

To see the effect of an edit, run the baseline and the edited version with the same inputs and compare the
catalogs. Match variables by `(variable_name, source_id, zone, heading, column_index)` and compare only `value` /
`values`. **Do not compare whole variables**: `sxo_file` and `metadata.generated_at` differ on every run. Unedited
runs are deterministic — two baseline runs give identical numbers.

```python
def key(v):
    return (v["variable_name"], v.get("source_id"), v.get("zone"), v.get("heading"), v.get("column_index"))

def numbers(catalog):
    return {key(v): (v.get("value"), v.get("values")) for v in catalog["variables"]}

base, edited = numbers(baseline), numbers(edited_catalog)
changed = [k for k in base if k in edited and base[k] != edited[k]]
added, removed = edited.keys() - base.keys(), base.keys() - edited.keys()
```

**Watch for warning tables.** Some edits make SIMIEN add warnings to the result (headings starting with
`Advarsel:`, e.g. "Advarsel: Tanktemperaturen er lavere enn turtemperatur til varmebatteriet"). They shift the
tables that follow, so most keys stop matching and `changed` can be empty while `added`/`removed` are large. When
that happens, compare the figures you care about directly, by `variable_name` and `zone` — for example the net
energy total:

```python
def total(catalog):
    v = next(v for v in catalog["variables"] if v["variable_name"] == "Energibehov" and v.get("zone") == "Alle soner")
    return dict(zip(v["labels"], v["values"]))["Totalt netto energibehov, sum 1-6"]
```

---

## 7. Run every simulation — `POST /api/v1/sim/simulate-all`

Same as `simulate` without `params`: it finds every simulation in the file (year, winter, …) and runs them in
parallel. Evaluations (energy label, profitability) are not run here; use `simulate` for those.

| Field | Required | Description |
|---|---|---|
| `file` | yes | Your `.simx` (§2.2) |
| `simien_version` | no | Read from the file when omitted; an invalid value returns `400 INVALID_REQUEST` |
| `location` | no | Climate override |
| `format` | no | What goes inside the ZIP: `catalog` (default) or `sxo` |
| `simulatable_parameters` | no | Your edits, applied before every simulation |

The response is always `{file}_simulations.zip`: one file per successful simulation plus `_results.json`:

```json
{
  "job_id": "…", "sxi_file": "building.simx", "simien_version": "8.1.1.15", "climate": "Oslo", "format": "catalog",
  "simulation_count": 2, "succeeded": 2, "failed": 0,
  "applied_changes": {"changed": 1, "applied": ["…"]},
  "simulations": [
    {"type": "wintersim", "id": "winter-simulation#159", "status": "success", "file": "….json", "variable_count": 404, "seconds": …},
    {"type": "yearsim",   "id": "year-simulation#158",   "status": "success", "file": "….json", "variable_count": 610, "seconds": …}
  ]
}
```

A failing simulation does not stop the others; it is listed with `"status": "failed"` and an `error`. The request
only fails (`502 ALL_SIMULATIONS_FAILED`) if every simulation fails. Headers: `X-Job-ID`, `X-Simulation-Count`
(succeeded), `X-Failed-Count`, `X-Simulatable-Parameters-Changed`.

---

## 8. Describe a building — `POST /api/v1/sim/simx-metadata`

Send a `.simx`, get a JSON file (`{name}_metadata.json`) for display. Nothing is simulated.

```jsonc
{
  "filename": "building.simx",
  "project": {"name": "…", "company": "…", "version": "8.1.1.15"},
  "climate_name": "Oslo",
  "detected_simien_version": "8_1_1_15",
  "simulation_count": 3,
  "simulations": [{"type": "yearsim", "id": "year-simulation#158", "name": "Year Simulation 1"}, …],
  "zone_count": 16, "total_area": 518.8,
  "zones": [
    {"id": "zone#6", "name": "living room and kitchen", "building_type": "Småhus", "area": 66.08, "volume": 190.31,
     "surfaces": [{"id": "roof#40", "name": "living_room_and_kitchen_89bc1d78..Face13_Roof", "uvalue": 0.1, "windows": [], "doors": []}, …],
     "ventilation_systems": […], "heating_systems": […], "cooling_systems": []}
  ],
  "central_heating_systems": […], "central_cooling_systems": […], "measures": […],
  "solar_panels": […], "wind_generators": […]
}
```

Areas are given once, on the zone; `total_area` is their sum.

---

## 9. Simulatable parameters: change numbers and re-simulate

This is how you change a `.simx`: you never edit the file itself. Two calls, and **the same file goes in both**:

```
POST /api/v1/sim/simulatable-parameters   file=building.simx
    → every editable number, the simulation ids, the version and source.sha256

POST /api/v1/sim/simulate                 file=building.simx  simulatable_parameters=edit.json
    → the result, with the edits applied, and X-Simulatable-Parameters-Changed: <n>
```

The building is never stored or modified on the server; edits are applied in memory for that one request.

### 9.1 The document — `POST /api/v1/sim/simulatable-parameters`

| Input | Description |
|---|---|
| `file` | Your `.simx` (§2.2) |
| `download` (query) | `true` returns it as `{name}_simulatable_parameters.json`; default is inline JSON |

```jsonc
{
  "source": {
    "filename": "building.simx",
    "sha256": "29b6901aa58674bb…",          // identity of this building — copy it into your edits
    "simien_version": "8.1.1.15",
    "schema_version": "8.1.2.22", "schema_exact": false, "generated_at": "…", "job_id": "…"
  },
  "warnings": [],                            // parse problems in the file — read these (§11.2)
  "simulations": [{"id": "year-simulation#158", "type": "yearsim", "name": "Year Simulation 1"}, …],
  "limits": {"facades": {"uvalue": {"min": 0.01, "max": 10.0, "unit": "W/m2K"}, …}, …},
  "parameters": {
    "zones": [ { "id": "zone#6", "label": "Zone 'living room and kitchen'",
                 "facades": [ { "id": "facade#41", "label": "Facade 'living_room_and_kitchen_89bc1d78..Face2_ExteriorWall'",
                                "values": {"uvalue": 0.45, "thermal_cap": 2.4, "ext_absorption_coeff": 0.8},
                                "replaced_by_measure": {"measure": "measure#160", "measure_name": "TEK 17 upgrade",
                                                        "replacement": "facade#178"} }, … ],
                 "ventilation_systems": [ … ], "internal_gains": [ … ], … } ],
    "measures": [ { "id": "measure#160", "label": "Measure 'TEK 17 upgrade'",
                    "values": {"investment": 10000.0, "maintenance": 0.0, "lifespan": 15.0},
                    "facades": [ { "id": "facade#178", "values": {"uvalue": 0.18, …} }, … ],
                    "zones": [ … ], "windows": [ … ], … } ],
    "central_heating_systems": [ … ], "global_variables": [ … ]
  },
  "excluded": {"by_rule": {"geometry": 1050, "schema_says_string": 3004, …}}
}
```

The tree follows the building (`zones[] → facades[] → windows[]`, `ventilation_systems[]`, `internal_gains[]`, …;
plus `measures[]`, `central_heating_systems[]`, `global_variables[]`). A measure holds its own copies of the objects
it changes (`measures[] → facades[]`, `windows[]`, `zones[]`, …). Each object has an `id`, a `label`, a `comment`,
and one or more of:

| Key | What it holds | Example |
|---|---|---|
| `values` | Plain numbers, one per attribute | `{"uvalue": 0.18, "thermal_cap": 2.4}` |
| `profile` | A daily schedule: **exactly 96** quarter-hour values, 00:00 → 24:00 | ventilation, lighting, occupancy |
| `series` | Parallel arrays over `index` | power-tariff steps; monthly electricity prices |
| `active: false` | The object is stored in the file but not used by the simulation — edits to it are accepted but change nothing | a zone following the NS 3031 standard profile |
| `replaced_by_measure` | An accepted measure in the file has its own copy of this object, and year and winter simulations use the copy. Edit `replacement` instead | `{"measure": "measure#160", "measure_name": "TEK 17 upgrade", "replacement": "facade#178"}` |

Only numbers inside `values`, `profile` and `series` can be changed. Geometry, names, ids, dates and the
simulation/climate settings are not in the document, and objects cannot be added or removed.

### 9.2 The minimal document

Send only what you change. It has two top-level keys:

```jsonc
{
  "source": {"sha256": "<source.sha256 from simulatable-parameters>"},
  "parameters": [
    {"id": "<object id>", "values":  {"<attribute>": <number>}},
    {"id": "<object id>", "profile": {"<slot>": <number>}},
    {"id": "<object id>", "series":  {"<series name>": {"<position>": <number>}}}
  ]
}
```

| Key | What to put there |
|---|---|
| `source.sha256` | **Required.** Copied exactly from the response for this building |
| `parameters` | A flat list, one entry per object you change (a nested tree also works — only `id`s are read) |
| `id` | Copied from the document: `facade#178`, `window#183`, `cav#222/usage_ventilation`, `project#29680/power_steps` |
| `values` | Attribute → new number, using the attribute names in that object's `values` |
| `profile` | Slots by number (`"0"`–`"95"`) or name (`"percentage_0730-0745"`), or all 96 values (`null` = unchanged) |
| `series` | Series name → positions by index name (`"mar"`) or number, or the full array (`null` = unchanged) |

A real multi-edit on the house, applied as 8 changes:

```json
{
  "source": {"sha256": "29b6901aa58674bb704f76729ac4593255aa8defe5f0e37e10d392ce9214d446"},
  "parameters": [
    {"id": "facade#178", "values": {"uvalue": 0.10}},
    {"id": "facade#179", "values": {"uvalue": 0.10, "thermal_cap": 3.0}},
    {"id": "window#183", "values": {"uvalue": 0.6}},
    {"id": "cav#222/usage_ventilation", "profile": {"percentage_0730-0745": 80}},
    {"id": "project#29680/power_steps", "values": {"hours": 4.0}, "series": {"cost": {"2": 400.0}}},
    {"id": "facade#41", "values": {"uvalue": 0.10}}
  ]
}
```

It moved 200 of 610 result variables (room heating 7,038 → 6,743 kWh). The last entry, `facade#41`, was applied but
had no effect, because an accepted measure replaces it with `facade#178` (§9.6).

Windows, doors, heaters and profiles are objects with their own `id`: list them as their own entries, not inside
their parent. Validate your document against `/schemas/simulatable-parameters.schema.json` before sending — it
catches strings instead of numbers, missing `id`s and short profiles; the server additionally checks that each `id`
exists, the attribute is editable, and the value is inside `limits`.

| Do | Don't | Otherwise |
|---|---|---|
| `"uvalue": 0.18` | `"uvalue": "0.18"` or `true` | `400 PARAMETERS_REJECTED` ("is not a number") |
| `{"id": "facade#178", "values": {"uvalue": 0.10}}` | `{"id": "facade#178", "uvalue": 0.10}` | Silently ignored: changed = 0 |
| Copy `id` from the document | Build it yourself | `400 PARAMETERS_REJECTED` ("no such object in this file") |
| Attribute names from that object's `values` | `area`, or a window attribute on a facade | `400 PARAMETERS_REJECTED` ("not an editable parameter") |
| `sha256` from this building's response | A document made for another file | `400 PARAMETERS_WRONG_BUILDING` |
| Send the document as a file field | Send it as a text field | `422 VALIDATION_ERROR` |

### 9.3 Send it back

```bash
curl -X POST "$API/simulate" -H "$AUTH" \
  -F "file=@building.simx" \
  -F "simulatable_parameters=@edit.json;type=application/json" \
  -F "params=year-simulation#158" -F "simien_version=8.1.1.15" -F "format=catalog" -o edited.json
```

The same field works on `simulate-all`. Sending the full document back unchanged is valid and changes nothing
(changed = 0); sending it with one number edited is the same as sending a minimal document with that number.

### 9.4 Profiles and series

```jsonc
{"id": "cav#222/usage_ventilation", "profile": {"percentage_0730-0745": 80}}   // by slot name
{"id": "cav#222/usage_ventilation", "profile": {"30": 80}}                     // by slot number (30 = 07:30–07:45)
{"id": "cav#222/usage_ventilation", "profile": [null, null, …, 80, …, null]}   // all 96, null = unchanged
{"id": "project#29680/power_steps", "series": {"cost": {"2": 400.0}}}          // by position
{"id": "project#…/monthly_el_price", "series": {"import_price": {"mar": 3.4}}} // by index name, where the file has monthly prices
```

A short array (e.g. 95 values) is rejected rather than guessed at.

### 9.5 Check what was changed

- **`X-Simulatable-Parameters-Changed`** counts changed *values*: one attribute = 1, one profile slot = 1, so
  setting a whole profile counts 96. `0` means your numbers equalled the file (a wrong `id` would have been a 400).
- **The list of changes** comes with the result:

| Where | Format |
|---|---|
| `applied_changes` at the top of the catalog | `simulate`, `format=catalog` |
| `_applied_changes.json` in the ZIP | `simulate`, `format=zip` |
| `applied_changes` in `_results.json` | `simulate-all` |
| not available (count header only) | `simulate`, `format=sxo` |

```json
{
  "changed": 3,
  "applied": [
    {"address": "facade#178", "attribute": "uvalue", "old": 0.18, "new": 0.1,
     "label": "Facade 'living_room_and_kitchen_89bc1d78..Face2_ExteriorWall'", "active": true},
    {"address": "cav#222/usage_ventilation", "attribute": "percentage_0730-0745", "old": 100.0, "new": 80.0,
     "position": 30, "label": "Ventilation system 'CAV - Constant Air Volume 1', ventilation profile", "active": true},
    {"address": "facade#41", "attribute": "uvalue", "old": 0.45, "new": 0.1,
     "label": "Facade 'living_room_and_kitchen_89bc1d78..Face2_ExteriorWall'", "active": true,
     "replaced_by_measure": {"measure": "measure#160", "measure_name": "TEK 17 upgrade", "replacement": "facade#178"}}
  ]
}
```

Each entry names the object (`label`) and says whether it can move the result: `active: false` or a
`replaced_by_measure` means the edit was written but the simulation doesn't use that object (§9.6).
A `repairs` list is added only when the file had to be fixed before simulating — an attribute repeated on one tag
(e.g. `"line 539: removed duplicate attribute language"`), or a wrong encoding declaration. Repairs are not your
changes.

### 9.6 Check the effect

"Applied" means the number was written into the file; it does not guarantee the result moves. Compare against a
baseline run (§6.3). Real examples:

| Edit | Changed | Result variables that moved | Net energy demand |
|---|---|---|---|
| House: `facade#178` u-value 0.18 → 0.10 | 1 | 160 of 610 | room heating 7,038 → 6,912 kWh |
| House: all 28 facades → 0.10 | 28 | 224 of 610 | room heating −15 % |
| House: door u-value 1.0 → 3.0 | 1 | 193 of 610 | total +4.7 % |
| House: heating setpoint 22 → 19 °C | 2 | 209 of 610 | total −2.6 % |
| House: the original `facade#41` (replaced by the "TEK 17 upgrade" measure's `facade#178`) | 1 | **0** of 610 | none — flagged `replaced_by_measure` |
| Office building: all 73 facades → 0.10 | 69 | 356 of 839 | room heating −43 %, room cooling +20 % |
| Office building: ventilation schedule → 50 % all day | 96 | 117 of 839 | fans −7.8 % |

**"Applied" is not "effective."** An edit that is applied but moves nothing usually has one of these causes:

- **`active: false`** — the object is stored in the file but ignored by the simulation (e.g. an internal-gain
  profile of a zone that follows the NS 3031 standard).
- **`replaced_by_measure`** — an accepted measure has its own copy of the object, and year and winter simulations
  use the copy. Edit the `replacement` instead. (Profitability evaluations compare with and without the measure,
  so they still react to the original.)
- **The setting doesn't apply in this building's configuration.** Some values only matter in certain setups, and
  the document can't tell you which: e.g. a window's constant solar factor, a local tap-water heater's temperature
  when tap water comes from the central system, or the heaters of a heating system that a measure replaces (the
  system itself is flagged; its heaters are not).

So: always run a baseline and compare (§6.3) before drawing conclusions from an edit.

### 9.7 What gets rejected

All of these return `400` before anything is simulated, with every problem listed at once in
`error.message.problems`. A file that isn't a SIMIEN project at all returns `400 FILE_UNREADABLE` instead.

| Code | Cause | Example `problems` entry |
|---|---|---|
| `PARAMETERS_INVALID` | Not valid JSON, or no `source.sha256` | "The parameters file is not valid JSON" |
| `PARAMETERS_WRONG_BUILDING` | `sha256` belongs to a different building | (`expected_sha256` / `uploaded_sha256` in the message) |
| `PARAMETERS_REJECTED` | Unknown id | "facade#9999: no such object in this file" |
|  | Not a number | "facade#178.uvalue: '0.1' is not a number" |
|  | Outside limits | "facade#179.uvalue: 20.0 is outside the allowed range 0.01..10.0" |
|  | Not editable on this object | "facade#180.area: not an editable parameter of this object" |
|  | Edits without an id | "parameters[4]: this object has edits but no \"id\" …" |
|  | Profile length | "cav#222/usage_ventilation.profile: expected 96 values, got 95 …" |
|  | Unknown position | "project#29680/power_steps.series.cost: \"smarch\" is not a position of this array — use one of 1, 2, 3, 4… or 0..4" |
|  | Unknown series | "project#29680/power_steps.series.gas_price: not an editable series of this object" |
|  | Profile on an object without one | "facade#181: this object has no profile" |

### 9.8 Complete example (Python)

```python
import json, requests

API  = "https://central-api.simien.no/api/v1/sim"
AUTH = {"Authorization": "Bearer " + API_KEY}
DATA = open("building.simx", "rb").read()

def upload():                                   # the same bytes every time
    return {"file": ("building.simx", DATA, "application/octet-stream")}

def post(path, **kw):
    r = requests.post(f"{API}{path}", headers=AUTH, timeout=300, **kw)
    if r.status_code >= 400:
        err = r.json()["error"]
        raise RuntimeError(f"{err['code']}: {err['message']} (request {err['request_id']})")
    return r

# 1. what can I change, and what can I run?
doc = post("/simulatable-parameters", files=upload()).json()
sha, version = doc["source"]["sha256"], doc["source"]["simien_version"]
sim = next(s["id"] for s in doc["simulations"] if s["type"] == "yearsim")

# 2. every facade the simulation actually uses (skip inactive ones and ones a measure replaces)
def walk(node):
    if isinstance(node, dict):
        yield node
        for v in node.values():
            yield from walk(v)
    elif isinstance(node, list):
        for v in node:
            yield from walk(v)

facades = [n for n in walk(doc["parameters"])
           if str(n.get("id", "")).startswith("facade#") and "uvalue" in n.get("values", {})
           and n.get("active") is not False and not n.get("replaced_by_measure")]

# 3. baseline and edited run
form = {"params": sim, "simien_version": version, "format": "catalog"}
baseline = post("/simulate", files=upload(), data=form).json()

edit = {"source": {"sha256": sha},
        "parameters": [{"id": f["id"], "values": {"uvalue": 0.10}} for f in facades]}
r = post("/simulate", data=form,
         files={**upload(), "simulatable_parameters": ("edit.json", json.dumps(edit), "application/json")})
edited = r.json()
print(r.headers["X-Simulatable-Parameters-Changed"], "values changed")

# 4. the effect on net energy demand
def energy(catalog):
    v = next(v for v in catalog["variables"] if v["variable_name"] == "Energibehov" and v.get("zone") == "Alle soner")
    return dict(zip(v["labels"], v["values"]))

before, after = energy(baseline), energy(edited)
for label in before:
    if before[label] != after[label]:
        print(f"{label}: {before[label]} → {after[label]} kWh")
```

---

## 10. Versions and locations

- `GET /api/v1/sim/versions` (no parameters) →
  `{"count": 92, "versions": [{"version": "8_1_2_22", "core_version": "8.1.2.22", "platform": "x86", "build_type": "release"}, …]}`,
  newest first. Pass `version` or `core_version` as `simien_version`.
- `GET /api/v1/sim/locations` (no parameters) → `{"count": 359, "locations": ["Bergen", "Bodø", …, "Oslo", …]}`.
  Pass a name exactly as listed.

---

## 11. Errors and troubleshooting

### 11.1 Error codes

| Code | Status | Meaning | What to do |
|---|---|---|---|
| `MISSING_API_KEY` | 401 | No `Authorization: Bearer` header | Send the key |
| `INVALID_API_KEY` | 401 | The key is not valid | Check the key |
| `API_KEY_EXPIRED` | 401 | The key has expired | Ask us for a new key |
| `CUSTOMER_SUSPENDED` | 403 | The account is suspended | Contact us |
| `VALIDATION_ERROR` | 422 | A required field is missing or has the wrong type | Fix the fields listed in `details` |
| `RATE_LIMIT_EXCEEDED` | 429 | Too many requests to this endpoint | Wait `retry_after` seconds |
| `INVALID_REQUEST` | 400 | Rejected before simulating: bad version, wrong file type, empty file, two ids, bad `format`, no simulations in the file | Read `message` |
| `SIMX_DECRYPT_FAILED` | 400 | The `.simx` could not be decrypted | Check it is a valid, untampered `.simx` |
| `SIMX_KEYS_UNAVAILABLE` | 502 | The service could not load its `.simx` decryption keys; not a problem with your file | Retry later; contact us with the `request_id` if it persists |
| `FILE_UNREADABLE` | 400 | The building file could not be read | Check the file |
| `PARAMETERS_INVALID` | 400 | Edits are not valid JSON or have no `source.sha256` | §9.7 |
| `PARAMETERS_WRONG_BUILDING` | 400 | Edits were made from a different building | Call `simulatable-parameters` on this file again |
| `PARAMETERS_REJECTED` | 400 | Some edits cannot be applied | Fix every entry in `message.problems` |
| `SIMULATION_REJECTED` | 400 | The simulation engine refused the file or settings | Read `message.error` — §11.2 |
| `SXO_UNREADABLE` | 400 | The file sent to `sxo/*` is not a SIMIEN result | Send a `.sxo` from `simulate` or SIMIEN |
| `FILE_TOO_LARGE` | 413 | The file is over the 50 MB limit | Send a smaller file |
| `ALL_SIMULATIONS_FAILED` | 502 | Every simulation in `simulate-all` failed | Read each entry in `message.failed_simulations` |
| `SIMULATION_TIMEOUT` | 504 | The engine did not answer in time | Retry later |
| `SIMULATION_SERVICE_UNAVAILABLE`, `UPSTREAM_UNAVAILABLE`, `UPSTREAM_ERROR` | 502 | The simulation service is unavailable | Retry later with backoff |
| `UPSTREAM_AUTH_FAILED` | 502 | Configuration problem on our side | Contact us |
| `INTERNAL_ERROR` | 500 | Unexpected error | Contact us with the `request_id` |

### 11.2 Common problems

| Symptom | Cause | Fix |
|---|---|---|
| `warnings` in `simulatable-parameters`, e.g. "line 539: Attribute language redefined" | The file has an XML defect. An attribute repeated on one tag is repaired automatically before simulating (listed under `repairs`) | Nothing, for repeated attributes |
| `SIMULATION_REJECTED`, `error`: "The request body is not well-formed XML (line …)" | An XML defect that can't be repaired automatically | Fix and re-save the file in SIMIEN |
| `SIMX_DECRYPT_FAILED` | The file is not a valid `.simx` (damaged, or not a SIMIEN export) | Export the `.simx` from SIMIEN again |
| `FILE_UNREADABLE` | The file is not a SIMIEN project (wrong or damaged file) | Check you sent the right file |
| `SIMULATION_REJECTED`, "Unknown location 'X'" | Location name not in the list | Use a name from `/locations` exactly |
| `SIMULATION_REJECTED`, "Invalid or unknown CoreVersion" | Version the engine doesn't have | Use `source.simien_version` or `/versions` |
| `SIMULATION_REJECTED`, "No sim node with id '…'" | Simulation id not in this file | Use an id from `simulations[]` |
| `INVALID_REQUEST`, "File must be a .simx file" | `simx-metadata` got a `.sxi` | Use `simulatable-parameters` for `.sxi` files; a `.sxi` is already plaintext |
| `INVALID_REQUEST`, "Only .sxo files are accepted" | `sxo/*` got a building instead of a result | Send the `.sxo` from `simulate` (`format=sxo`) |
| Edit applied (changed ≥ 1) but results identical | `active: false`, `replaced_by_measure`, or a setting that doesn't apply here | §9.6 |
| Changed = 0 | Values equal the file, or attributes outside `values` / `profile` / `series` | §9.2 |
| 429 on one endpoint | Rate limit for that endpoint | Wait `retry_after`; other endpoints still work |

---

## 12. Rules for automated clients and LLM agents

Follow these exactly; they cover every mistake seen in testing.

1. Base path `https://central-api.simien.no/api/v1/sim/`; send `Authorization: Bearer <key>` on every call.
2. The building is normally a `.simx` (encrypted). Send it as it is; don't decrypt it first. Change it only through
   simulatable parameters, never by editing XML. A plaintext `.sxi` works the same way.
3. Start with `POST /simulatable-parameters` on the building. Take `source.sha256`, `source.simien_version` and
   `simulations[].id` from it — never invent ids or versions.
4. File requests are `multipart/form-data`: the building in field `file`; edits as a **second file field**
   `simulatable_parameters` (JSON), never a text field.
5. `params` is exactly one simulation id; use `simulate-all` for all of them.
6. Use a timeout of at least 120 s for `simulate` (more for `simulate-all`); results can be 10+ MB.
7. Prefer `format=catalog` when you need numbers and the list of applied changes.
8. Edits: a minimal document `{"source": {"sha256": …}, "parameters": [{"id": …, "values": {…}}]}`. Only change
   numbers inside `values`, `profile`, `series`; keep ids exactly; stay inside `limits`; skip objects with
   `"active": false`; for objects with `replaced_by_measure`, edit the `replacement` id instead. Validate against
   `/schemas/simulatable-parameters.schema.json` first.
9. Send the edits with the **same** building file they were made from.
10. After an edited run, check `X-Simulatable-Parameters-Changed` and `applied_changes`; then compare numbers with a
    baseline run (match variables by name, source, zone, heading, column; ignore `sxo_file`) to confirm an effect.
11. "Applied" is not "effective": never report an edit's effect without comparing against a baseline run. If an
    applied edit moves nothing, tell the user which of the causes in §9.6 applies instead of retrying.
12. On an error, branch on `error.code`. For `PARAMETERS_REJECTED` fix every entry in `error.message.problems` in
    one go. Don't retry a `400` unchanged.
13. On `429` wait `retry_after` seconds; on `502`/`504` retry with backoff.
14. If `simulatable-parameters` returns `warnings`, the simulation may still work (repeated attributes are repaired
    automatically and listed under `repairs`); if it fails with "not well-formed XML", report the warning to the user
    instead of retrying.
