Writing Results Data
Environmental results (impact assessment, resource use, output flows, carbon flows) are written as part of the EPD document via:
PUT /api/v2/epds/{epd_id}/document/
This page documents the shape of that results data — the one part of the document whose keys are validated against the EPD's PCR spec rather than free-form.
Where results live
Each results table is a top-level document key holding a list of result sets. Most EPDs have exactly one set per table:
| Document key | Table | Rows come from |
|---|---|---|
impacts | Environmental Impact Assessment | the PCR spec |
resource_uses | Resource Use Indicators | the PCR spec |
output_flows | Waste and Output Flow Indicators | the PCR spec |
carbon_flows | Carbon Emissions and Removals | the PCR spec |
additional_results | Additional Indicators (optional) | the indicator catalog |
/schema/ returns the field id (resource-uses, hyphens); the document key
these tables are bound to is resource_uses (underscores). Only impacts is
spelled the same both ways, which makes the difference easy to miss.
Either spelling is accepted on write, and validation reads both (it tries the bound key, then falls back to the field id). What differs is what comes back:
| Endpoint | Returns |
|---|---|
GET /epds/{id}/ | your document verbatim — exactly the keys you sent |
GET /epds/{id}/view/ | the same, plus the bound-key alias |
So a table written as resource-uses reads back as resource-uses from the
detail endpoint, not resource_uses — it is not missing. Pick one spelling and
use it consistently; the bound key is the better choice, because it is the one
both endpoints agree on. The spec's bindings_json is the mapping.
Each set is an object with a results-data matrix and an optional discussion (HTML string):
{
"impacts": [
{
"discussion": "<p>Cradle-to-gate impacts are dominated by module A1.</p>",
"results-data": {
"GWP-total": { "a1a2a3": 173.83, "a4": 3.94, "c2": 1.99, "d": -90.19 },
"ODP": { "a1a2a3": 0.00000865, "a4": 0.00000007, "d": -0.00000417 }
}
}
]
}
The results-data matrix
results-data maps indicator id → module key → value.
- Indicator ids (rows) must be indicators declared by the EPD's PCR spec (on the Additional Indicators table they come from the catalog instead) — fetch them from
GET /api/v2/epds/{epd_id}/schema/(theresults_matrixitem field inside each resultssection_listlistsindicatorswithid,label, andunit). Typical EN 15804+A2 ids:GWP-total,GWP-fossil,GWP-biogenic,GWP-luluc,ODP,AP,EP-freshwater,EP-marine,EP-terrestrial,POCP,ADP-minerals&metals,ADP-fossil,WDP,PM,IRP,ETP-fw,HTP-c,HTP-nc,SQP. - Module keys (columns) are lifecycle module codes:
a1,a2,a3,a4,a5,b1–b7,c1–c4,d— plus the aggregated production columna1a2a3(A1-A3 combined), which is always accepted. - Values may be JSON numbers or numeric strings (scientific notation is fine). Units are not written per value — each indicator's unit comes from the PCR spec and renders automatically.
Key casing
Canonical module keys are lowercase (a1a2a3, a4), and indicator ids use the spec's exact spelling (GWP-total). The API is tolerant on write: any casing is accepted (A1A2A3, gwp-total) and the server canonicalizes keys before storing, so integrations that already send uppercase module keys keep working unchanged. What you read back via GET is the canonical form.
What gets rejected
The PCR spec is the contract for this matrix, so two things fail the whole document write with 400 results_matrix_invalid (violation details included per key):
- an indicator id the spec doesn't declare (
unknown_indicator), and - a module key that is neither a module declared in
information_modulesnora1a2a3(unknown_module).
Two other things also fail the write: a malformed matrix or reserved map (results_matrix_shape), and, on a variant EPD, a value that drifts from a field its reference EPD locked. Everything else about the document is validated softly — the save succeeds and issues are reported in constraint_violations on the EPD detail.
Declared modules and production display
Two sibling document fields control which columns exist and how the production stage displays:
information_modules— the declared lifecycle modules, as a lowercase list, e.g.["a1","a2","a3","a4","a5","c2","c3","c4","d"]. This is what makes a module a legal column key.production_stage_show_as— how the production stage displays; see the next section.
If the display setting and the data disagree (e.g. "separated" declared but only a1a2a3 reported), nothing is lost — the editor and digital view render every column that actually holds data — but keeping them consistent renders the cleanest tables.
Production stage display
production_stage_show_as decides how modules A1, A2 and A3 appear in every results table, the PCR tables and the Additional Indicators table alike:
| Value | Columns |
|---|---|
aggregated (default) | one a1a2a3 column, shown as A1-A3 |
separated | a1, a2, a3 |
both | a1, a2, a3, then a1a2a3 (the aggregate column follows A3 everywhere: editor, view, PDF, workbook) |
The document may hold both kinds of keys whatever the display says; validation accepts a1a2a3 alongside the declared modules. The grids never hide a column that already holds a value, so switching from aggregated to separated keeps a populated A1-A3 column visible. Under aggregated and separated the print document additionally drops A1, A2 or A3 columns that are empty beside a populated aggregate; under both all four columns print.
Optional: label overrides
A reserved _labels key inside results-data renames indicators for display (e.g. translations) without changing their ids:
"results-data": {
"GWP-total": { "a1a2a3": 173.83 },
"_labels": { "GWP-total": "Climate change (total)" }
}
Keys inside _labels must be declared indicator ids (unknown_label_override rejects otherwise).
How PCR table units and methods are resolved
Every row of the four PCR tables is tied to the LCIA indicator catalog through a catalog_id on the spec's indicator definition. Inventory flows carry one id (FW → fw) because they mean the same thing under every reporting convention. Impact rows whose characterization method follows the route carry one id per results-convention value:
{ "id": "AP", "unit": "mol H+ eq", "label": "AP",
"catalog_id": { "en15804a2": "ap@ef-3.1", "iso21930": "ap@traci-2.2" } }
The document never changes: rows stay keyed by the spec id (AP). At render time the digital view, the PDF and the editor grid look up the entry for the EPD's results_convention (falling back to the spec's default) and take the unit and methodology from the catalog, so an ISO 21930 EPD shows AP in kg SO2 eq under TRACI 2.2 while an EN 15804+A2 EPD shows mol H+ eq under EF 3.1. A row with no entry for the current convention keeps the spec's own unit and shows no method. The spec's unit is that fallback; spec_catalog_drift() reports where it disagrees with the catalog.
A PCR that replaces a table through an amend_field override (NSF 1103 swaps in its own impact rows — GWP-total and GWP-biogenic under IPCC AR6, the other categories under TRACI 2.1 — and locks the convention to ISO 21930) ties those rows in its own seed JSON; a locked convention always wins over the document value. A row may also name its methodology outright with lca_method (NSF 1103's GWP-20, which has no catalog entry); that wins over the catalog's.
The Additional Indicators table (catalog mode)
Specs derived from the base EPD input spec declare a fifth, optional results table — additional-results (document key additional_results) — for indicators the PCR does not prescribe. Its results_matrix runs in catalog mode (indicator_source: "catalog" in the schema): rows are picked from the normalized LCIA indicator catalog rather than typed. A row key is a catalog id, <indicator>@<method>, and the label, unit and methodology are resolved from the catalog when the EPD renders — so a unit is a property of the (method, indicator) pair, never something you send.
{
"additional_results": [
{
"discussion": "<p>Reported per the program operator's regional method.</p>",
"results-data": {
"ap@traci-2.2": { "a1a2a3": 0.0021, "a4": 0.00004 },
"wdp@ef-3.1": { "a1a2a3": 12.4 }
}
}
]
}
- Which ids are legal: any entry of the catalog whose method is in the table's
catalog_methods, plus every method-less flow (pere,hwd, …), which are always legal. Anything else isunknown_indicatorand fails the write. See Reading the catalog for how to fetch the legal set. - No
_labels/_unitsfor catalog rows: the catalog owns both; sending them for a catalog id isunknown_label_override/unknown_unit_override. The two maps still work for indicators the spec itself declares on the table. - Columns stay locked:
unknown_moduleapplies exactly as on the PCR tables. - The catalog is versioned; entries are never deleted, so an id you store keeps resolving. Entries marked
status: "provisional"are usable — only their unit is still under methodology review, and a corrected unit shows on every EPD automatically because units are never stored per document.
An entry marked selectable: false in the catalog (today, the TRACI 2.1 freshwater/marine eutrophication aliases) still resolves for rows that already use it, but should not be offered for new rows — treat it as read-only in your own pickers.
Reading the catalog
Two endpoints serve the catalog. Prefer the standalone one: it is cacheable, independent of any EPD, and the only one on the summary schema path.
GET /api/v2/specs/indicator-catalog/ # whole catalog
GET /api/v2/specs/indicator-catalog/?methods=traci-2.2,ef-3.1 # filtered slice
{
"catalog_version": "1.0.0",
"methods": [
{ "id": "traci-2.2", "name": "TRACI 2.2", "ec3_method": "TRACI 2.2" }
],
"entries": [
{
"id": "ap@traci-2.2",
"method": "TRACI 2.2",
"method_id": "traci-2.2",
"indicator": "AP",
"label": "Acidification Potential",
"unit": "kg SO2 eq",
"family": "impacts",
"status": "verified",
"ec3": { "method": "TRACI 2.2", "key": "ap", "unit": "kgSO2e" }
},
{
"id": "pere",
"method": null,
"method_id": null,
"indicator": "PERE",
"label": "PERE",
"unit": "MJ",
"family": "resources",
"status": "verified",
"standard": "en15804",
"aliases": ["Renewable primary energy — energy use"],
"ec3": { "method": null, "key": null, "unit": "MJ" }
}
]
}
| Entry key | Meaning |
|---|---|
id | The row key you write into results-data. <indicator>@<method> for an impact, a bare slug for a method-less flow. |
method / method_id | Display name and id of the characterization method; both null for inventory flows. |
indicator / label | Short code and display name. Neither is a valid row key — always write id. |
unit | Resolved at render time. Never send it; _units on a catalog row is rejected. |
family | impacts, resources, waste or carbon — useful for grouping a picker. |
status | verified, or provisional while a unit is still under methodology review. Provisional entries are fully usable. |
selectable | Present and false only on entries kept for existing data. Do not offer these for new rows. |
aliases | Alternative names, for matching rows imported from elsewhere. |
ec3 | The corresponding EC3 method, key and unit spelling, when one exists. |
methods[] gives you the picker's first level; filter entries[] by method_id for the second. Filtering the request by ?methods= never removes the method-less flows — they belong to every slice.
The per-EPD alternative is the full schema view, which carries the slice a given EPD's table permits:
GET /api/v2/epds/{epd_id}/schema/ # full view: includes `indicator_catalog`
GET /api/v2/epds/{epd_id}/schema/?view=summary # does NOT include it
indicator_catalog is null when the EPD's spec declares no catalog-mode table. Because the summary view omits it, an integration built on ?view=summary (the view these docs otherwise recommend) should read the catalog from the standalone endpoint.
Free mode (legacy)
A results_matrix may instead declare indicator_source: "free" (or the legacy allow_custom_indicators: true). On such a table you define the rows: any id not starting with _ is accepted, and the reserved _units map ({indicator id: unit string}, the sibling of _labels) is the only source of a unit for an indicator the spec does not declare. Ids starting with _ are rejected (results_matrix_shape). On a spec-locked table (indicator_source absent or "spec") _units keys must be declared indicators, mirroring _labels, and the spec's unit always wins for a declared indicator.
Both modes render after the PCR tables in the digital EPD and the PDF, and round-trip through the project XLSX export/import (the lateral editor's workbook). The editor's own "Download/Upload All Results" template covers the four PCR tables only.