Revit add-in · contract v2.0
API contract
Everything the add-in calls, the shape of every answer, what each error code means, and which fields changed.
Conventions
- Base URL
- https://eurostructura.base44.app
- Auth — add-in
- Authorization: Bearer rv_… — the key the add-in is given when the user signs in (POST /functions/revitPluginLogin). Sent on every add-in call. A key can also be created by hand under Profile.
- Auth — HUD views
- A short-lived view token in the request body. The HUD pages send only that; the key never goes in a URL.
- Success
- { success: true, contractVersion, message, …payload }
- Error
- { success: false, error, code, details?, contractVersion } — the HTTP status matches the code, and details names the field when one is at fault.
- Method
- POST with a JSON body, except the project list. Parameters only in the body — URL parameters are not read.
- Missing data
- A block that has no data behind it reports nulls and a named reason in dataState.missing. Zeros are only ever real measured zeros.
/functions/revitPluginLoginnone — the approve step runs in the user's browser sessionThe add-in's sign-in. It asks for a code, opens verifyUrl in the browser where the signed-in user approves once, then polls until the key is handed over. Nothing is typed by hand, and the key is collected exactly once.
Request
{ "action": "start", "device": "DESKTOP-7F2K" }
{ "action": "poll", "deviceCode": "dv_…" }Response
{
"success": true, "message": "Sign-in started.",
"deviceCode": "dv_…", "userCode": "K7QP3M2X",
"userCodeDisplay": "K7QP-3M2X",
"verifyUrl": "https://eurostructura.base44.app/revit-plugin/link?code=K7QP3M2X",
"expiresAt": "2026-09-27T09:45:00.000Z",
"expiresInSeconds": 600, "pollIntervalSeconds": 3
}
{ "success": true, "message": "Waiting for approval.", "status": "pending" }
{ "success": true, "message": "Signed in.", "status": "approved",
"token": "rv_…", "account": "you@example.com", "deviceLabel": "DESKTOP-7F2K" }
Open verifyUrl (or show userCodeDisplay for the user to type) and poll every 3 s.
The token is handed over once — after that the device code answers
DEVICE_NOT_FOUND, so store the key and stop polling./functions/revitPluginProjectsBearer rv_The projects this key's owner can reach, for the add-in's project picker.
Response
{
"success": true,
"contractVersion": "2.0",
"message": "2 projects.",
"projects": [
{
"id": "6ab6c73d075ca6d839c625c9",
"name": "house 2",
"location": "Malta",
"updated": "2026-09-26T09:12:00.000Z"
}
]
}/functions/revitPublishModelBearer rv_Files a model snapshot against a project — the publish action. No IFC file is sent: the model's elements travel in elementRecords, with their dimensions, materials and quantities, and they replace the project's stored element inventory — which is what the project page's embodied-carbon assessment reads. Without them the project keeps only the summary figures. Everything you send is stored and read straight back, so the project page and every read API show the real figures instead of zeros. Each publish becomes one version, numbered by revision.
Request
{
"projectId": "6ab6c73d075ca6d839c625c9",
"model": "house 2",
"elements": 1319,
"totals": { "volumeM3": 412.5, "areaM2": 1890, "weightKg": 1240800 },
"categories": { "Walls": 1128, "Floors": 191 },
"byMaterial": { "Concrete C30/37": 980000, "Steel S355": 42000 },
"elementRecords": [
{
"name": "Wall 01",
"category": "masonry",
"material_key": "brick",
"material_name": "Clay brick",
"ifc_type": "Wall",
"ifc_global_id": "3kF6g0I5",
"mode": "qty",
"qty": 12.8,
"unit": "m3",
"area_m2": 64,
"volume_m3": 12.8
}
],
"utilisation": {
"byCategory": { "Walls": 0.86, "Floors": 0.14 },
"bySystem": { "Gravity": 0.66, "Lateral": 0.41 }
},
"meta": { "source": "revit-plugin", "pluginVersion": "1.4.0", "modelUnits": "metric" },
"timestamp": "2026-09-26T16:00:00.000Z"
}Response
{
"success": true,
"contractVersion": "2.0",
"message": "Published to house 2.",
"snapshotId": "6ab7f1…",
"projectId": "6ab6c73d075ca6d839c625c9",
"projectName": "house 2",
"publishedAt": "2026-09-26T16:00:00.000Z",
"elementCount": 1319,
"revision": 7,
"elementSync": { "replaced": true, "stored": 1319 },
"stored": {
"totals": { "volumeM3": 412.5, "areaM2": 1890, "weightKg": 1240800, "elements": 1319 },
"categories": { "Walls": 1128, "Floors": 191 },
"materials": { "Concrete C30/37": 980000, "Steel S355": 42000 },
"utilisation": { "byCategory": { "Walls": 0.86, "Floors": 0.14 } },
"calculations": {},
"meta": { "source": "revit-plugin", "pluginVersion": "1.4.0", "modelUnits": "metric" }
}
}
A block that arrives but cannot be read fails the publish with
SNAPSHOT_PARSE_FAILED and details naming the field — a publish never succeeds
with silently empty data behind it./functions/revitHudDataview token or Bearer + projectIdThe aggregated read: every block in one call, so the add-in renders its tabs without scraping several endpoints. Pass view to narrow it to one block, and basis to choose the heatmap basis.
Request
{ "projectId": "6ab6c73d075ca6d839c625c9" }
{ "projectId": "6ab…", "view": "heatmap", "basis": "carbon_intensity" }
{ "viewToken": "vh_…" }Response
{
"success": true,
"contractVersion": "2.0",
"message": "Everything published for house 2.",
"project": { "id": "6ab…", "name": "house 2" },
"hasSnapshot": true,
"snapshot": {
"id": "6ab7f1…", "revision": 7,
"publishedAt": "2026-09-26T16:00:00.000Z",
"modelName": "house 2", "elementCount": 1319,
"totals": { "elements": 1319, "volumeM3": 412.5, "areaM2": 1890, "weightKg": 1240800 },
"hasTotals": true,
"categories": { "Walls": 1128, "Floors": 191 },
"categoryCount": 2, "byMaterial": { "Concrete C30/37": 980000 },
"materials": { "Concrete C30/37": 980000 },
"materialCount": 1, "meta": { "pluginVersion": "1.4.0" },
"hasCalculations": false, "utilisationGroupCount": 2
},
"dataState": {
"hasSnapshot": true, "hasElementDetail": false,
"hasUtilisation": true, "hasCarbon": false,
"sources": { "totals": "snapshot", "heatmap": "snapshot",
"utilisation": "snapshot", "carbon": null },
"missing": ["NO_ELEMENT_DETAIL", "NO_CARBON_ASSIGNMENT"]
},
"calculations": { "…": "see the calculations schema" },
"heatmap": { "…": "see the heatmap schema" },
"utilisation": { "…": "see the utilisation schema" },
"lca": { "…": "see the LCA schema" },
"views": ["calculations", "heatmap", "utilisation", "lca", "all"]
}/functions/revitHudData · view: "calculations"view token or Bearer + projectIdModel totals and the engineering calculations. Totals come from the published snapshot where you sent them — the element database is only a fallback — so a published project never reads back as zeros.
Request
{ "projectId": "6ab…", "view": "calculations" }Response
{
"success": true, "message": "Model outputs for house 2.",
"totals": { "elements": 1319, "volumeM3": 412.5, "areaM2": 1890, "weightKg": 1240800 },
"units": { "elements": "elements", "volumeM3": "m³", "areaM2": "m²", "weightKg": "kg" },
"basis": {
"source": "published snapshot",
"method": "Totals exactly as the add-in reported them for the published model.",
"elementCount": 0, "snapshotElementCount": 1319,
"publishedAt": "2026-09-26T16:00:00.000Z"
},
"categories": [{ "name": "Walls", "count": 1128, "volumeM3": 0 }],
"materials": [{ "name": "Concrete C30/37", "count": 980000, "volumeM3": 0 }],
"calculations": { "…": "see the calculations schema" }
}/functions/revitHudData · view: "heatmap"view token or Bearer + projectIdThe heatmap dataset, with a selectable basis, a legend, and one entry per element (or per category when only a snapshot exists).
Request
{ "projectId": "6ab…", "view": "heatmap", "basis": "utilisation_ratio" }Response
{
"success": true, "message": "Density by utilisation ratio.",
"basis": "utilisation_ratio", "basisLabel": "utilisation ratio",
"source": "snapshot",
"availableBases": ["utilisation_ratio", "count"],
"min": 0.14, "max": 0.86, "range": 0.72,
"scale": { "basis": "utilisation_ratio", "min": 0.14, "max": 0.86, "unit": "ratio" },
"legend": [
{ "id": "minimal", "label": "Minimal", "color": "#f5f5f4",
"from": 0, "to": 0.2, "valueFrom": 0.14, "valueTo": 0.28, "unit": "ratio" }
],
"buckets": [{ "label": "Walls", "count": 1128, "value": 0.86, "intensity": 1 }],
"elements": [
{ "id": null, "name": "Walls", "category": "Walls", "value": 0.86,
"unit": "ratio", "intensity": 1, "bucket": "critical", "color": "#dc2626" }
],
"missing": ["NO_ELEMENT_DETAIL"],
"bases": ["utilisation_ratio", "carbon_intensity", "volume", "count"]
}
Per-element entries need the model's elements in the project database; without
them the dataset is per category from the snapshot, and says so in missing./functions/revitLcaBearer rv_ or view tokenThe full embodied-carbon report under EN 15978: totals by module, material, category and lifecycle stage, the biggest contributors, intensity, and what to do next.
Request
{
"projectId": "6ab6c73d075ca6d839c625c9",
"assumptions": { "transportDistanceKm": 100, "freightEf": 0.107, "eolDistanceKm": 50 },
"overrides": { "concrete-c30": { "a13": 0.09 } }
}Response
{
"success": true, "message": "Embodied carbon for house 2.",
"unit": "kgCO2e",
"totals": {
"kgCO2e": 184320.5, "tCO2e": 184.32, "massKg": 1240800,
"modules": { "a13": 170100, "a4": 13280, "a5": 6800, "b": 0,
"c1": 1200, "c2": 660, "c3": 980, "c4": 1300, "c": 4140, "d": -88000 }
},
"byStage": [
{ "id": "A1-A3", "label": "A1–A3 · Product", "kgCO2e": 170100, "share": 0.923 },
{ "id": "D", "label": "D · Beyond the system boundary", "kgCO2e": -88000, "share": -0.477 }
],
"topContributors": [{ "key": "concrete-c30", "label": "Concrete C30/37",
"kgCO2e": 100940, "share": 0.548 }],
"benchmarks": {
"intensityKgCO2ePerM2": 97.5,
"basis": "gross area as published for this project",
"external": null,
"note": "No external benchmark set is configured, so this is the project's own intensity."
},
"recommendations": [
{ "code": "DOMINANT_MATERIAL", "priority": "high",
"title": "Concrete C30/37 carries 55% of the embodied carbon",
"detail": "100940 kgCO₂e of 184320 kgCO₂e. …", "materialKey": "concrete-c30" }
],
"method": {
"standard": "EN 15978",
"modules": "A1–A3 + A4 + A5 + B1–B5 + C1–C4, with D declared separately",
"database": "ICE v3.0 (Circular Ecology, 2019) + cited sector EPDs",
"version": "ICE-v3.0", "timberBiogenic": "excluded", "overrides": 1,
"assumptions": ["Transport to site: 100 km by road.", "…"]
},
"quality": { "coverage": 0.949, "confidence": "high", "flags": ["…"] }
}/functions/revitSnapshotsBearer rv_ or view tokenThe project's published history. Returns the newest snapshot plus a page of history, newest first. Page backwards with before; fetch one version in full with snapshotId.
Request
{ "projectId": "6ab…", "limit": 20, "before": "2026-09-26T16:00:00.000Z", "snapshotId": "6ab7f1…" }Response
{
"success": true, "message": "2 snapshots.",
"project": { "id": "6ab…", "name": "house 2" },
"latest": { "id": "6ab7f1…", "revision": 7, "publishedAt": "2026-09-26T16:00:00.000Z",
"modelName": "house 2", "elementCount": 1319,
"totals": { "elements": 1319, "volumeM3": 412.5 },
"hasTotals": true, "categories": { "Walls": 1128 },
"categoryCount": 1, "materialCount": 1,
"utilisationGroupCount": 2 },
"snapshots": ["…newest first, same shape…"],
"count": 2, "limit": 20, "before": null,
"hasMore": false, "historyTruncated": false
}/functions/revitHudSessionBearer rv_Exchanges the API key for a short-lived view token and the HUD paths. Open them as BaseUrl + path.
Request
{ "projectId": "6ab6c73d075ca6d839c625c9" }Response
{
"success": true, "message": "View session ready.",
"viewToken": "vh_…", "expiresAt": "2026-09-26T16:15:00.000Z",
"expiresInSeconds": 900,
"projectId": "6ab…", "projectName": "house 2",
"paths": {
"calculations": "/plugin-hud/calculations?view=vh_…",
"heatmap": "/plugin-hud/heatmap?view=vh_…",
"utilisation": "/plugin-hud/utilisation?view=vh_…"
}
}Data model
Field-by-field shapes for the five payloads the add-in writes and reads. Types are indicative — validate at runtime, and ignore fields you do not recognise.
1 · Snapshot — what a publish stores
POST /functions/revitPublishModel → ModelSnapshot
{
id, project_id, owner_id, revision, published_at,
model_name, element_count, publisher, source,
totals: { elements?, volumeM3?, areaM2?, weightKg? },
categories: { [categoryName]: number },
byMaterial: { [materialName]: number | { count, volumeM3, weightKg } },
materials: { ...same data as byMaterial for backward compatibility... },
// Optional: when sent, these replace ProjectElement rows for this project.
elementRecords: [{
name, category, material_key?, material_name?,
ifc_global_id?, ifc_type?,
mode: "dims" | "qty",
length_mm?, width_mm?, height_mm?,
qty?, unit?: "kg" | "t" | "m3",
area_m2?, volume_m3?, weight_kg?,
components?: [{ category?, label?, material_key, share? }],
keynote?, keynote_note?, description?
}],
utilisation: { [groupKey]: { [label]: number | { value, unit?, status? } } },
calculations: { [calculationId]: object }, // as sent by the add-in
meta: { source?, pluginVersion?, modelUnits? }
}2 · Calculations
revitHudData · view: "calculations" → calculations
{
standard: { primary, alternatives, note },
totals: { elements, volumeM3, areaM2, weightKg },
units: { … },
totalsSource: "snapshot" | "elements" | null,
loads: {
deadLoad: { value, unit, codeRef, method, inputs, source },
liveLoad: { value: null, unit, codeRef, method, missing: ["occupancy or use class"] },
snowLoad: { value: null, unit, codeRef, method, missing: [...] },
windLoad: { value: null, unit, codeRef, method, missing: [...] }
},
combinations: [
{ id: "ULS-1", expression: "1.35G + 1.5Q", codeRef: "EN 1990 Table A1.2(B)",
value: null, unit: "kN/m2", missing: ["deadLoad", "liveLoad"] }
],
demand: { worstRatio, label, unit, status, source } | null,
missing: ["NO_SNAPSHOT", "NO_ELEMENT_DETAIL"],
note
}
A load is computed when the project holds the data for it — dead load is
(ΣW × g) / A from the published weight and area — and otherwise comes back
null with the exact inputs that would unlock it. Nothing is guessed.3 · Heatmap
revitHudData · view: "heatmap"
{
basis: "utilisation_ratio" | "carbon_intensity" | "volume" | "count",
basisLabel, basisDescription, source: "elements" | "snapshot" | "none",
availableBases: [...], bases: [...],
min, max, range,
scale: { basis, basisLabel, min, max, unit },
legend: [{ id, label, color, from, to, valueFrom, valueTo, unit }],
buckets: [{ label, count, value, intensity }],
elements: [{ id, name, category, materialKey, materialName, ifcType, globalId,
value, unit, intensity, bucket, color, count? }],
truncated, missing, note
}
intensity is 0–1 across the observed range; bucket names the legend band and
color is its hex, so a view overlay can colour elements without recomputing
anything.4 · Utilisation
revitHudData · view: "utilisation"
{
hasData,
groups: [{ key, label, count, max, risk, worst: { label, value, unit },
rows: [{ label, value, unit, status, reported }] }],
thresholds: { warn, fail, appliesTo, source, note },
summary: { groups, rows, ok, warn, fail },
worst: { label, value, unit, status } | null
}
status / risk: ok | warn | fail. Revit's own status is kept where it sent one;
otherwise a unitless ratio is compared with the thresholds. Rows carrying a
unit are never thresholded.5 · LCA
POST /functions/revitLca · revitHudData → lca
{
unit: "kgCO2e",
totals: { kgCO2e, tCO2e, massKg, modules: { a13, a4, a5, b, c1, c2, c3, c4, c, d } },
byMaterial: [{ key, label, source, efA13, wastePct, density, count,
massKg, kgCO2e, share, a13, a4, a5, c, d }],
byCategory: [{ name, count, massKg, kgCO2e, share }],
byStage: [{ id, label, kgCO2e, share }],
topContributors: [{ key, label, kgCO2e, share }],
benchmarks: { intensityKgCO2ePerM2, basis, external, note },
recommendations: [{ code, priority, title, detail, materialKey? }],
method: { standard, modules, database, version, timberBiogenic, overrides, assumptions },
quality: { elementCount, assessedElements, unassignedElements, missingQuantityElements,
compositeElements, coverage, confidence, truncated, flags: [{ code, count, message }] }
}Calculation standard & assumptions
- Structural calculations
- Eurocode declared per calculation: EN 1991-1-1 for self-weight and imposed loads, EN 1991-1-3 for snow, EN 1991-1-4 for wind, combinations per EN 1990 Tables A1.1 and A1.2(B). ASCE 7-16 is named as the alternative.
- What is computed today
- Dead load — the published model's own weight over its published area, (ΣW × g) / A — and demand/capacity ratios, taken from the utilisation Revit computed.
- What is declared but not computed
- Live, snow and wind loads. Each returns null with the exact inputs it needs (occupancy class; altitude, snow zone, roof and exposure coefficients; terrain category, reference height, basic wind velocity). They are never estimated.
- Carbon
- EN 15978, modules A1–A3 + A4 + A5 + B1–B5 + C1–C4 with D declared separately and never offsetting the headline. Factors from ICE v3.0 (Circular Ecology, 2019) and the sector EPDs cited per material; timber biogenic carbon excluded.
Migration map
- totals
- Now stored and read back. Previously accepted by the endpoint and dropped, which is why published projects read as zeros. No field change needed — send it and it appears.
- byMaterial
- Stored as materials. Accepted as a map of name → number, or name → { count, volumeM3, weightKg }, or an array of entries. Nothing else about it changes.
- meta
- Now stored (source, pluginVersion, modelUnits) and returned on every snapshot read. No field change.
- categories
- Unchanged in name. Values may now be a number or an object with count and volumeM3; both are accepted.
- utilisation
- Unchanged in name. A row may be a bare number or { value, unit, status } as before.
- VIEW_TOKEN_INVALID / VIEW_TOKEN_EXPIRED
- Renamed SESSION_INVALID / SESSION_EXPIRED. Error responses include legacyCode (VIEW_TOKEN_*) for compatibility; branch on code first.
- error envelope
- Now always carries success: false alongside error and code. Additive — existing readers keep working.
Error codes
- AUTH_MISSING401No Authorization header, or it is not a Bearer token.
- AUTH_INVALID401The key does not match any key on the account.
- RATE_LIMITED429More than 60 requests in a minute on this key, or too many sign-ins waiting.
- AUTH_REQUIRED401The approve/deny step needs a signed-in ArchiStructura account.
- DEVICE_NOT_FOUND404The device code is unknown, already collected, or the sign-in was started elsewhere.
- DEVICE_EXPIRED410Ten minutes passed without approval — start the sign-in again.
- CODE_INVALID404The approval code is unknown, already used or expired.
- CODE_DENIED403The sign-in was refused in the browser.
- TOKEN_LIMIT400Ten add-ins are already connected — revoke one in the profile first.
- PROJECT_REQUIRED400projectId was missing or blank.
- PROJECT_NOT_FOUND404That project is not owned by, or shared with, the key's owner.
- SNAPSHOT_NOT_FOUND404No snapshot with that id on this project.
- SNAPSHOT_PARSE_FAILED422A block arrived but could not be read — details names the field.
- PAYLOAD_INVALID400A required field was missing or the wrong type — see details.
- SESSION_INVALID401The view token is unknown — reopen the view from the add-in.
- SESSION_EXPIRED401The view token is past its fifteen minutes.
- SERVER_ERROR500The app failed while handling the request — details carries the reason.
Versioning policy
- Where the version lives
- Every success and error envelope carries contractVersion. Nothing else in the payload is versioned.
- Within a version
- Changes are additive only. New fields and new flag codes appear without notice, so ignore anything you do not recognise.
- Breaking changes
- A field being removed, renamed, retyped or given a new meaning raises the version. The add-in should refuse a major version it does not know.
- Error codes
- Codes are stable and are never reused for a different meaning. New codes may be added inside a version — branch on the code, never on the message text.
- Snapshot revisions
- revision counts publishes within one project (1 = first) and is unrelated to contractVersion.
cURL
# Sign in — ask for a code and open the returned verifyUrl
curl -X POST https://eurostructura.base44.app/functions/revitPluginLogin \
-H "Content-Type: application/json" \
-d '{"action":"start","device":"DESKTOP-7F2K"}'
# …then poll every 3 s until status is "approved" (the token arrives once)
curl -X POST https://eurostructura.base44.app/functions/revitPluginLogin \
-H "Content-Type: application/json" \
-d '{"action":"poll","deviceCode":"dv_…"}'
# Project list
curl -H "Authorization: Bearer rv_…" \
https://eurostructura.base44.app/functions/revitPluginProjects
# Publish a model snapshot — send everything you have
curl -X POST https://eurostructura.base44.app/functions/revitPublishModel \
-H "Authorization: Bearer rv_…" -H "Content-Type: application/json" \
-d '{"projectId":"6ab6…","model":"house 2","elements":1319,
"totals":{"volumeM3":412.5,"areaM2":1890,"weightKg":1240800},
"categories":{"Walls":1128,"Floors":191},
"byMaterial":{"Concrete C30/37":980000},
"utilisation":{"byCategory":{"Walls":0.86}},
"meta":{"pluginVersion":"1.4.0"},
"timestamp":"2026-09-26T16:00:00.000Z"}'
# Everything at once
curl -X POST https://eurostructura.base44.app/functions/revitHudData \
-H "Authorization: Bearer rv_…" -H "Content-Type: application/json" \
-d '{"projectId":"6ab6…"}'
# One block, one basis
curl -X POST https://eurostructura.base44.app/functions/revitHudData \
-H "Authorization: Bearer rv_…" -H "Content-Type: application/json" \
-d '{"projectId":"6ab6…","view":"heatmap","basis":"carbon_intensity"}'
# Full carbon report
curl -X POST https://eurostructura.base44.app/functions/revitLca \
-H "Authorization: Bearer rv_…" -H "Content-Type: application/json" \
-d '{"projectId":"6ab6…"}'
# Published history
curl -X POST https://eurostructura.base44.app/functions/revitSnapshots \
-H "Authorization: Bearer rv_…" -H "Content-Type: application/json" \
-d '{"projectId":"6ab6…","limit":20}'
# Open a HUD view session
curl -X POST https://eurostructura.base44.app/functions/revitHudSession \
-H "Authorization: Bearer rv_…" -H "Content-Type: application/json" \
-d '{"projectId":"6ab6…"}'