REST API for building energy simulation using the Bemify calculation engine. Upload a SIMIEN Pro model (.sxi) and get back energy performance results, energy labels (A-G), and TEK17 compliance checks.
Base URL: https://api.bemify.no
# Run a simulation with server-side climate data
curl -X POST https://api.bemify.no/simulate \
-H "Authorization: Bearer bmf_YOUR_TOKEN" \
-F "model=@building.sxi" \
-F "klimasted=Oslo"
# Poll for results
curl https://api.bemify.no/job/job_123456_1 \
-H "Authorization: Bearer bmf_YOUR_TOKEN"Note (Windows): Use
curl.exe --ssl-no-revoke. The Windows build of curl performs a TLS certificate revocation check that fails against Let's Encrypt certificates.
All simulation endpoints require a Bearer token in the Authorization header:
Authorization: Bearer bmf_YOUR_TOKEN
Contact erlend@bemify.no for API access.
Start a new simulation. Returns a job ID for polling.
Content-Type: multipart/form-data
| Parameter | Type | Required | Description |
|---|---|---|---|
model |
file | Yes | SIMIEN Pro project file (.sxi) |
climate |
file | No | EnergyPlus weather file (.epw). Not allowed with energimerke or tek17. |
klimasted |
string | No | Municipality name (e.g. Oslo, Bergen). Alternative to climate. Not allowed with tek17. |
simuleringstype |
string | No | aarssimulering (default), energimerke, or tek17 |
Climate data rules:
- Provide either
klimastedorclimatefile, not both - For
tek17: climate data is always the TEK17 reference climate. Do not provideklimastedorclimate; the request will be rejected with 400 - For
energimerke: onlyklimastedis accepted — EPW files are rejected with 400. Energy labeling requires a standard Norwegian climate location for the climate correction factor - Use
GET /klimastederto list valid municipality names
Per-key limits:
POST /simulate: max 6 requests per minute per API keyGET /job/:jobId: max 120 requests per minute per API key- Max 3 active jobs (
queued+running) per API key
Response (202):
{
"jobId": "job_1712832645123_1",
"position": 1,
"message": "Simulering lagt i kø (posisjon 1). Poll /job/job_1712832645123_1 for status."
}Validate an SXI file without running the simulation. Returns a structured list of errors, warnings, and missing fields detected during parsing and conversion to the Bemify intermediate format. Useful for iterating on model files before committing to a full simulation.
Content-Type: multipart/form-data
| Parameter | Type | Required | Description |
|---|---|---|---|
model |
file | Yes | SIMIEN Pro project file (.sxi) |
Per-key limits:
POST /validate: max 20 requests per minute per API key
Response (200):
{
"isValid": true,
"errors": [],
"warnings": [
{
"nodeId": "zone-1",
"field": "setpoint",
"message": "Default value used",
"defaultValue": 21
}
],
"missingData": [
{
"nodeId": "zone-1",
"nodeType": "Sone",
"missingFields": [
{ "field": "buildingSimulationProperties", "defaultValue": null }
]
}
]
}isValid: false means the file could be parsed but contains structural errors,
the response is still 200. HTTP 400 is only returned when the file cannot be
parsed at all (malformed XML, missing .sxi-file, etc.).
Check job status and retrieve results. Requires authentication.
Statuses: queued | running | completed | error
Response when queued (200):
{
"jobId": "job_1712832645123_1",
"status": "queued",
"queuedAt": "2026-04-11T10:30:45.123Z",
"queueLength": 3
}queueLength is the current queue depth (only included for queued status).
Response when completed (200):
Response when error (200):
{
"jobId": "job_1712832645123_1",
"status": "error",
"completedAt": "2026-04-11T10:32:15.789Z",
"error": "Error message"
}Results are kept for up to 30 minutes after completion, but may be deleted earlier if the in-memory completed-job cap is reached.
List all available climate locations. No authentication required.
Response (200):
{
"locations": ["TEK17 Referanseklima", "Oslo", "Bergen", "Trondheim", ...],
"count": 51
}Server health check. No authentication required.
Response (200):
{
"status": "ok",
"timestamp": "2026-04-11T10:30:45.123Z",
"queue": { "length": 0, "processing": false }
}Queue status. No authentication required.
Response (200):
{ "queueLength": 0, "isProcessing": false }| Value | Description | Climate | Extra result fields |
|---|---|---|---|
aarssimulering |
Full year simulation (default) | klimasted or EPW | energimerke |
energimerke |
Energy labeling | klimasted only | energimerke |
tek17 |
TEK17 compliance check | Automatic (reference, no input allowed) | energimerke, tek17 |
Computed automatically when the project has a valid municipality and building category.
{
"energimerke": "B",
"totalArea": 250.0,
"korreksjonsfaktor": 1.05,
"klimakorrigertVektetSpesifikk": 92.3,
"sumVektetSpesifikk": 96.9,
"sumLevertEnergi": 24075,
"sumSpesifikk": 96.3,
"vektetKlimaavhengig": 42.1,
"vektetIkkeKlimaavhengig": 54.8,
"items": [
{
"kilde": "1 Levert elektrisitet",
"levertEnergi_kWh": 21500,
"spesifikk_kWhm2": 86.0,
"vektingsfaktor": 1.0,
"vektetSpesifikk_kWhm2": 86.0
}
]
}| Field | Unit | Description |
|---|---|---|
energimerke |
A-G | Energy label grade |
klimakorrigertVektetSpesifikk |
kWh/(m2*year) | Climate-corrected weighted specific delivered energy (determines grade) |
sumVektetSpesifikk |
kWh/(m2*year) | Weighted specific delivered energy (before climate correction) |
sumLevertEnergi |
kWh/year | Total delivered energy |
korreksjonsfaktor |
- | Climate correction factor for municipality/building type |
items |
array | Breakdown per energy carrier |
Only present when simuleringstype=tek17.
{
"erSamsvarsende": true,
"energiramme": {
"poster": [
{ "post": "1a", "beskrivelse": "Romoppvarming", "spesifikk_kWhm2": 12.3 }
// ... en post per energipost
],
"totalBeregnet": 105.2,
"forskriftskrav": 115.0,
"status": "oppfylt",
"bygningskategori": "Kontorbygning"
},
"minstekrav": {
"rader": [
{ "bygningsdel": "U-verdi yttervegger", "faktiskVerdi": 0.18, "kravVerdi": 0.22, "status": "oppfylt" }
// ... en rad per bygningsdel
],
"samletStatus": "oppfylt"
},
"luftmengder": {
"rader": [
{ "beskrivelse": "Spesifikk vifteeffekt (SFP)", "faktiskVerdi": 1.50, "kravVerdi": 2.00, "status": "oppfylt" }
// ... en rad per krav
],
"samletStatus": "oppfylt"
},
"energiforsyning": {
"brukerFossilBrensel": false,
"fossilKilder": [],
"punkt2Gjelder": true,
"harSentralVarmesentral": true,
"sentralAndelProsent": 85.2,
"status": "oppfylt"
},
"oppsummering": {
"antallOppfylt": 4,
"antallIkkeOppfylt": 0,
"antallIkkeRelevant": 0
}
}Possible status values: "oppfylt", "ikke_oppfylt", "ikke_relevant".
Results are grouped into calculation points per NS 3031. Important: the points do not share a common shape. The field holding the annual per-category totals is named differently in each point. Read the correct field per point:
| Point | Key | Description | Annual totals field |
|---|---|---|---|
| A | netto |
Net energy demand (building needs) | energyResults |
| B | brutto |
Gross energy demand (including system losses) | energyResults |
| C | tilfort |
Supplied energy (from energy sources) | tilfortEnergi |
| D | levert |
Delivered energy (per energy carrier) | levertEnergi |
| D (nZEB) | nzebLevert |
Delivered energy, nZEB variant (excludes certain posts, no export credit) | levertEnergi |
energyResults and tilfortEnergi are keyed by energy post ("1a Romoppvarming",
"1b Ventilasjonsvarme", "2 Varmtvann", "3a Romkjøling", "3b Ventilasjonskjøling",
"5 Belysning", ...) in kWh/year.
levertEnergi (points D) is keyed by energy carrier ("1 Levert elektrisitet",
"3 Levert fjernvarme", "4 Levert fjernkjøling", ...) in kWh/year.
Full field list per point:
{
// A: netto, B: brutto -- keyed by energy post
"netto": {
"energyResults": { "1a Romoppvarming": 12345, "2 Varmtvann": 6789 },
"energyTotal": 0,
"powerDemand": {},
"peakTimestamps": {},
"totalPeakPower": {}
},
"brutto": {
// samme form som "netto"
},
// C: tilfort -- keyed by energy post
"tilfort": {
"tilfortEnergi": {},
"tilfortEnergiTotal": 0,
"levertEffekt": {},
"levertEffektTimestamps": {},
"levertEffektTotal": {}
},
// D: levert -- keyed by energy carrier
"levert": {
"levertEnergi": { "1 Levert elektrisitet": 59989 },
"total": 0,
"elektriskBudsjett": {},
"klimafordeling": {
"klimaavhengig": {},
"ikkeKlimaavhengig": {}
},
"oppvarmingOutput_kWh": {},
"kjoelingOutput_kWh": {},
"apentIldstedBio_kWh": 0,
"monthlyLevertPerKilde": {},
"monthlyMakseffektPerKilde": {}
},
"nzebLevert": {
// samme form som "levert"
}
}Beyond beregningspunkter, zones, energimerke and tek17, the result object also
contains the following top-level fields (mainly for detailed analysis; most integrations
only need the four above):
| Field | Description |
|---|---|
outputMode |
Always "aggregated" for API results |
sentralAndelSone |
Central supply share of heat demand per zone [kWh] |
distribusjonsOgAkkumuleringstap |
Annual distribution + accumulation losses [kWh] |
energyPerPostPerZone |
Annual energy per post, per zone [kWh] |
monthlyEnergyPerPostPerZone |
Monthly energy per post, per zone [kWh] |
monthlyThermalPerZone |
Monthly heating/cooling per zone [kWh] |
monthlyTemperaturesPerZone |
Monthly min/avg/max temperatures per zone |
quarterlyEffektPerZone / quarterlyTemperaturPerZone |
15-min power / temperature time series per zone |
ventilasjonStatsPerZone |
Ventilation stats per zone |
varmetapstallPerSone |
Heat loss coefficients per zone |
solcelleProduction / vindturbinProduction |
PV / wind production |
kildeSPF / kildeOutput |
SPF and thermal output per central energy source |
warnings |
Simulation warnings |
metadata |
{ simulationTime, totalSteps, stepDuration, totalHours } |
| Code | Meaning | Example |
|---|---|---|
| 400 | Bad request | Missing model file, invalid simuleringstype, both klimasted and climate provided, invalid file type |
| 401 | Unauthorized | Missing Authorization header |
| 403 | Forbidden | Invalid or deactivated API key |
| 404 | Not found | Job not found or expired |
| 413 | Payload too large | File exceeds 10 MB limit |
| 429 | Rate limited | Too many requests globally, too many requests for one API key, or too many active jobs for one API key |
| 502 | Bad gateway | Failed to fetch climate data from upstream |
| 503 | Queue full | Max 20 concurrent jobs |
On Windows, add --ssl-no-revoke to all curl.exe commands (see note under Quick Start).
# Standard simulation with municipality climate data
curl -X POST https://api.bemify.no/simulate \
-H "Authorization: Bearer bmf_YOUR_TOKEN" \
-F "model=@building.sxi" \
-F "klimasted=Oslo"
# Energy label simulation
curl -X POST https://api.bemify.no/simulate \
-H "Authorization: Bearer bmf_YOUR_TOKEN" \
-F "model=@building.sxi" \
-F "klimasted=Oslo" \
-F "simuleringstype=energimerke"
# TEK17 compliance check (climate data fetched automatically)
curl -X POST https://api.bemify.no/simulate \
-H "Authorization: Bearer bmf_YOUR_TOKEN" \
-F "model=@building.sxi" \
-F "simuleringstype=tek17"
# Simulation with custom EPW file
curl -X POST https://api.bemify.no/simulate \
-H "Authorization: Bearer bmf_YOUR_TOKEN" \
-F "model=@building.sxi" \
-F "climate=@oslo.epw"
# Validate an SXI file without running a simulation
curl -X POST https://api.bemify.no/validate \
-H "Authorization: Bearer bmf_YOUR_TOKEN" \
-F "model=@building.sxi"
# Poll for results
curl https://api.bemify.no/job/job_123456_1 \
-H "Authorization: Bearer bmf_YOUR_TOKEN"import requests
import time
API_URL = "https://api.bemify.no"
TOKEN = "bmf_YOUR_TOKEN"
headers = {"Authorization": f"Bearer {TOKEN}"}
# Start simulation
with open("building.sxi", "rb") as model:
resp = requests.post(
f"{API_URL}/simulate",
headers=headers,
files={"model": model},
data={"klimasted": "Oslo", "simuleringstype": "energimerke"},
)
job_id = resp.json()["jobId"]
print(f"Job started: {job_id}")
# Poll for results
while True:
status = requests.get(f"{API_URL}/job/{job_id}", headers=headers).json()
if status["status"] in ("completed", "error"):
break
print(f"Status: {status['status']}...")
time.sleep(2)
if status["status"] == "completed":
result = status["result"]
# Energy label
if "energimerke" in result:
em = result["energimerke"]
print(f"Energy label: {em['energimerke']}")
print(f"Weighted specific: {em['klimakorrigertVektetSpesifikk']:.1f} kWh/(m2*year)")
# TEK17 (only for simuleringstype=tek17)
if "tek17" in result:
tek = result["tek17"]
print(f"TEK17 compliant: {tek['erSamsvarsende']}")
else:
print(f"Error: {status['error']}")See scripts/test_api.py for a more complete example script.
| Limit | Value |
|---|---|
| Rate limit | 30 requests per minute |
Per-key POST /simulate |
6 requests per minute |
Per-key POST /validate |
20 requests per minute |
Per-key GET /job/:jobId |
120 requests per minute |
| Max active jobs per API key | 3 |
| Max file size | 10 MB per file |
| Max queue depth | 20 jobs |
| Simulation timeout | 10 minutes |
| Result TTL | 30 minutes |
{ "jobId": "job_1712832645123_1", "status": "completed", "queuedAt": "2026-04-11T10:30:45.123Z", "startedAt": "2026-04-11T10:30:50.456Z", "completedAt": "2026-04-11T10:32:15.789Z", "result": { "beregningspunkter": { "netto": { "energyResults": {}, "energyTotal": 0, "powerDemand": {}, /* ... */ }, "brutto": { "energyResults": {}, /* ... */ }, "tilfort": { "tilfortEnergi": {}, /* ... */ }, "levert": { "levertEnergi": {}, "klimafordeling": {}, /* ... */ }, "nzebLevert": { "levertEnergi": {}, /* ... */ } }, "zones": [ { "id": "sone-1", "navn": "Sone 1", "area": 150.0 } // ... en per sone ], "energimerke": { /* se egen seksjon */ }, "tek17": { /* se egen seksjon */ } } }