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).
Authorization: Bearer <api-key> on every call (§2.1)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.
Agents can also start from /llms.txt, generate a client from /openapi.json, and validate documents against the JSON Schema.
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.
What can I change, and what can I run?
POST simulatable-parameters with building.simx →
parameters.json
Run the simulation as it is
POST simulate with year-simulation#158 and 8.1.1.15 →
baseline.json (the baseline)
Write the change you want
edit.json: facade#178 gets a better wall (use the full sha256)
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
import os, json, requests
API = "https://central-api.simien.no/api/v1/sim"
AUTH = {"Authorization": f"Bearer {os.environ['API_KEY']}"}
# 1. What can I change, and what can I run?
with open("building.simx", "rb") as f:
r = requests.post(f"{API}/simulatable-parameters", headers=AUTH, files={"file": f})
r.raise_for_status()
parameters = r.json()
import { readFile } from "node:fs/promises";
const API = "https://central-api.simien.no/api/v1/sim";
const AUTH = { Authorization: `Bearer ${process.env.API_KEY}` };
const building = new Blob([await readFile("building.simx")]);
// 1. What can I change, and what can I run?
const form = new FormData();
form.append("file", building, "building.simx");
const parameters = await (await fetch(`${API}/simulatable-parameters`,
{ method: "POST", headers: AUTH, body: form })).json();
$Api = "https://central-api.simien.no/api/v1/sim"
$Auth = @{ Authorization = "Bearer $env:API_KEY" }
# 1. What can I change, and what can I run?
Invoke-RestMethod -Method Post "$Api/simulatable-parameters" -Headers $Auth `
-Form @{ file = Get-Item building.simx } -OutFile parameters.json
From parameters.json take three things:
{
"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}} … }
}
# 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
building = open("building.simx", "rb").read()
form = {"params": "year-simulation#158", "simien_version": "8.1.1.15", "format": "catalog"}
# 2. Run the simulation as it is (the baseline)
r = requests.post(f"{API}/simulate", headers=AUTH, data=form, timeout=300,
files={"file": ("building.simx", building)})
baseline = r.json()
# 3. Write the change you want: facade#178 gets a better wall
edit = {"source": {"sha256": parameters["source"]["sha256"]},
"parameters": [{"id": "facade#178", "values": {"uvalue": 0.10}}]}
# 4. Run it again with the change (the same .simx + the edit)
r = requests.post(f"{API}/simulate", headers=AUTH, data=form, timeout=300,
files={"file": ("building.simx", building),
"simulatable_parameters": ("edit.json", json.dumps(edit), "application/json")})
edited = r.json()
# 5. Confirm the change was applied, then compare baseline with edited (§6.3)
print(r.headers["X-Simulatable-Parameters-Changed"]) # → 1
const simulate = (edit) => {
const form = new FormData();
form.append("file", building, "building.simx");
form.append("params", "year-simulation#158");
form.append("simien_version", "8.1.1.15");
form.append("format", "catalog");
if (edit) form.append("simulatable_parameters",
new Blob([JSON.stringify(edit)], { type: "application/json" }), "edit.json");
return fetch(`${API}/simulate`, { method: "POST", headers: AUTH, body: form });
};
// 2. Run the simulation as it is (the baseline)
const baseline = await (await simulate()).json();
// 3. Write the change you want: facade#178 gets a better wall
const edit = { source: { sha256: parameters.source.sha256 },
parameters: [{ id: "facade#178", values: { uvalue: 0.10 } }] };
// 4. Run it again with the change (the same .simx + the edit)
const response = await simulate(edit);
const edited = await response.json();
// 5. Confirm the change was applied, then compare baseline with edited (§6.3)
console.log(response.headers.get("X-Simulatable-Parameters-Changed")); // → 1
$Run = @{ params = "year-simulation#158"; simien_version = "8.1.1.15"; format = "catalog" }
# 2. Run the simulation as it is (the baseline)
Invoke-RestMethod -Method Post "$Api/simulate" -Headers $Auth `
-Form ($Run + @{ file = Get-Item building.simx }) -OutFile baseline.json
# 3. Write the change you want: facade#178 gets a better wall
$sha256 = (Get-Content parameters.json -Raw | ConvertFrom-Json).source.sha256
@{ source = @{ sha256 = $sha256 }; parameters = @(@{ id = "facade#178"; values = @{ uvalue = 0.10 } }) } |
ConvertTo-Json -Depth 5 | Set-Content edit.json
# 4. Run it again with the change (the same .simx + the edit)
$r = Invoke-WebRequest -Method Post "$Api/simulate" -Headers $Auth -OutFile edited.json -PassThru `
-Form ($Run + @{ file = Get-Item building.simx; simulatable_parameters = Get-Item edit.json })
# 5. Confirm the change was applied, then compare baseline.json with edited.json (§6.3)
$r.Headers["X-Simulatable-Parameters-Changed"] # → 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. .sxiworks too. A plaintext.sxiis accepted everywhere a.simxis, with the same results — exceptsimx-metadata, which takes.simxonly.- The
sxo/*endpoints take a result (.sxo) in the samefilefield, 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 this 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:
{
"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.
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:
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:
{
"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.
{
"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 |
{
"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:
{
"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 ids 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:
{
"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 ids 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
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
{"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-Changedcounts changed values: one attribute = 1, one profile slot = 1, so setting a whole profile counts 96.0means your numbers equalled the file (a wrongidwould 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 |
{
"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 thereplacementinstead. (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)
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. Passversionorcore_versionassimien_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.
- Base path
https://central-api.simien.no/api/v1/sim/; sendAuthorization: Bearer <key>on every call. - 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.sxiworks the same way. - Start with
POST /simulatable-parameterson the building. Takesource.sha256,source.simien_versionandsimulations[].idfrom it — never invent ids or versions. - File requests are
multipart/form-data: the building in fieldfile; edits as a second file fieldsimulatable_parameters(JSON), never a text field. paramsis exactly one simulation id; usesimulate-allfor all of them.- Use a timeout of at least 120 s for
simulate(more forsimulate-all); results can be 10+ MB. - Prefer
format=catalogwhen you need numbers and the list of applied changes. - Edits: a minimal document
{"source": {"sha256": …}, "parameters": [{"id": …, "values": {…}}]}. Only change numbers insidevalues,profile,series; keep ids exactly; stay insidelimits; skip objects with"active": false; for objects withreplaced_by_measure, edit thereplacementid instead. Validate against/schemas/simulatable-parameters.schema.jsonfirst. - Send the edits with the same building file they were made from.
- After an edited run, check
X-Simulatable-Parameters-Changedandapplied_changes; then compare numbers with a baseline run (match variables by name, source, zone, heading, column; ignoresxo_file) to confirm an effect. - "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.
- On an error, branch on
error.code. ForPARAMETERS_REJECTEDfix every entry inerror.message.problemsin one go. Don't retry a400unchanged. - On
429waitretry_afterseconds; on502/504retry with backoff. - If
simulatable-parametersreturnswarnings, the simulation may still work (repeated attributes are repaired automatically and listed underrepairs); if it fails with "not well-formed XML", report the warning to the user instead of retrying.