# Smart EPD — full documentation
> Every hand-written page from docs.smartepd.com in one file. Curated index: https://docs.smartepd.com/llms.txt — endpoint reference is served separately as the OpenAPI spec: https://docs.smartepd.com/openapi/v2.json
---
Source: https://docs.smartepd.com/index.md
# Smart EPD Documentation
Smart EPD is a platform for authoring, validating, and managing **Environmental Product Declarations (EPDs)**. It connects manufacturers, LCA tool providers, verifiers, and consultants in a single workflow, from product data entry through third-party verification to public declaration.
## What's new in v2
Smart EPD v2 is a ground-up rebuild designed around how tool partners and manufacturers actually work at scale:
- **Organizational model** -A full hierarchy of Organizations, Workspaces, and Categories replaces the flat, project-by-project structure. Tool providers can collaborate across multiple manufacturer workspaces, each with its own scoped PCRs and verified tool configurations.
- **Specification engine with real inheritance** -A layered specification system where standards (ISO 21930, EN 15804+A2) flow into PCR-level specifications, then resolve into EPD Templates. Each layer inherits and can add requirements.
- **Streamlined verification** -Tool/model verification happens at the project level through the LCA submission and EPD template review. Once the LCA and EPD template are verified (along with approved constraints), variant EPDs can pass through automated validation. Verifier involvement in variant EPDs is only triggered when something is flagged.
- **API-first integration** -The full workflow is available via API. Tool providers can automate onboarding, submit EPD data programmatically, poll for status updates, and manage tool update requests.
## Who uses Smart EPD
Smart EPD serves four primary roles:
| Role | What they do |
|------|-------------|
| **Manufacturers** | Produce physical products that need EPDs. Own projects, fill in product data, submit for verification. |
| **Tool Providers** | Develop and maintain LCA/EPD calculation software. Register tools within Categories, define constraints, support manufacturers using their tools. |
| **Verifiers** | Review and certify specifications and EPDs for PCR compliance. For tool-based workflows, verify LCA and templates at the project level; for manual workflows, verify per-EPD. |
| **Consultants** | Perform LCA work and EPD creation on behalf of manufacturers. May or may not be involved in a given project. |
## How to use these docs
Start with the reading path that fits your role:
**New to the platform?** Begin with [Platform Overview](/getting-started/platform-overview) to learn the hierarchy and the two EPD workflows. Then read [Terminology](/concepts/terminology) for the full glossary.
**Working with templates and constraints?** Read [EPD Templating](/concepts/epd-templating) for how templates enforce value ranges, cross-field dependencies, and sensitivity analysis.
**Understanding the specification engine?** Read [Specifications & Validation](/concepts/specs-and-validation) for how the layered specification system works.
**Looking for the data model?** Read [Data Model](/concepts/data-model) for entity schemas and relationships, or [Roles & Access Control](/concepts/roles-and-access) for the permission model.
**Connecting an AI agent?** Read the [AI Agent Integration guide](/guides/agent-integration). These docs are also published in agent-ready formats — a curated index at [`/llms.txt`](https://docs.smartepd.com/llms.txt) and the full corpus at [`/llms-full.txt`](https://docs.smartepd.com/llms-full.txt).
---
Source: https://docs.smartepd.com/getting-started/platform-overview.md
# Platform Overview
Smart EPD organizes work through a five-level hierarchy. Each layer constrains the next, from who participates, to what rules apply, to what products are assessed, to the EPD output itself.
```mermaid
graph TD
ORG["Organization who you are"]
WS["Workspace who you work with"]
CAT["Category what tools and rules apply"]
PROJ["Project which product(s)"]
EPD["EPD the deliverable"]
ORG -->|"joins"| WS
WS -->|"contains"| CAT
CAT -->|"contains"| PROJ
PROJ -->|"contains"| EPD
style ORG fill:#3B82F6,color:#fff,stroke:#2563EB
style WS fill:#8B5CF6,color:#fff,stroke:#7C3AED
style CAT fill:#F59E0B,color:#fff,stroke:#D97706
style PROJ fill:#10B981,color:#fff,stroke:#059669
style EPD fill:#06B6D4,color:#fff,stroke:#0891B2
```
## Organization
An **Organization** represents a company or entity on the platform. Every user belongs to exactly one organization, and every action traces back to one. Organizations are assigned a type:
- **Manufacturing Group** -Produces physical products that need EPDs. A single enterprise may contain multiple manufacturing groups.
- **Tool Provider** -Develops and maintains LCA/EPD creation software (including Excel-based tools).
- **Consultant** -Performs LCA work and EPD creation on behalf of manufacturers.
- **Verifier** -Reviews and certifies specifications, EPD Templates, and EPDs for compliance.
## Workspace
A **Workspace** is a shared collaboration space where two or more organizations work together on EPD-related activities. Think of it as a joint venture room: it defines *who* is at the table and *what role* each organization plays.
Each organization has one role per workspace (e.g., Org A is the manufacturer, Org B is the tool provider, Org C is the verifier). An organization can participate in many workspaces simultaneously with different partners.
Workspaces govern **membership and permissions**; the actual content and rules live in Categories and projects.
## Category
A **Category** is the governed scope of work within a workspace. It defines the rules for a specific product category:
- **Which PCRs apply** -e.g., Part A / Part B1 / Part B2
- **Which tool is used** -for tool-based Categories, exactly one tool is registered
- **The PCR Specification** -the versioned definition of rules and requirements for the category
Categories come in two modes:
| | Tool-based Category | Manual Category |
|---|---|---|
| **Tool** | Required: a specific LCA tool is registered | None: external software used |
| **Verification** | Tool/model verified at the project level via LCA + template review | Verification happens per-EPD |
| **EPD Template** | Full specification chain with semver versioning, resolved per project | No template or very slim |
| **Scalability** | Verify once, produce many variant EPDs | Linear: each EPD reviewed individually |
The Category is the policy layer: it answers *"For this PCR, using this tool, what does a compliant EPD look like?"*
## Project
A **Project** is a single product assessment within a Category. It inherits its PCR and tool from the parent Category, and this is where the specification chain resolves into a concrete **EPD Template** with locked fields, unlocked fields, and validation constraints. The project defines:
- The specific product type, family, and line being assessed
- The team (individual users drawn from workspace member organizations)
- The EPD Template (resolved from the specification chain, verified by the verifier), including any input constraints required for variant EPDs
- One or more EPDs for that product
Projects are the day-to-day working unit. They can be duplicated within the same Category for similar products.
## EPD (Environmental Product Declaration)
The **EPD** is the deliverable: a standardized document declaring the environmental impact of a product. Smart EPD supports two distinct approaches depending on whether the Category is manual or tool-based.
### Traditional workflow (manual Categories)
In a manual Category, LCA practitioners use external software and submit each EPD individually for verifier review:
```mermaid
graph LR
DRAFT["Draft"] -->|"submit"| REVIEW["In Review"]
REVIEW -->|"approve"| VERIFIED["Verified"]
REVIEW -->|"request changes"| DRAFT
VERIFIED -->|"publish"| PUBLISHED["Published"]
style DRAFT fill:#6366F1,color:#fff,stroke:#4F46E5
style REVIEW fill:#F59E0B,color:#fff,stroke:#D97706
style VERIFIED fill:#10B981,color:#fff,stroke:#059669
style PUBLISHED fill:#06B6D4,color:#fff,stroke:#0891B2
```
Every EPD is individually reviewed by a verifier. This approach is appropriate for one-off assessments or low-volume product lines.
### Reference & variant workflow (tool-based Categories)
In a tool-based Category, the verifier certifies the *tool* once at the Category level, and then once per LCA/EPD template at the project level. This unlocks scalable EPD production with two distinct EPD types:
**Reference EPD** -Produced during project verification to setup subsequent variant EPDs with compliant output. It is a verification artifact, not a production document.
**Variant EPD** -The actual production EPD created by manufacturers for specific products. Because the tool, template, and constraints have been pre-verified, variant EPDs do not require individual verifier review; they pass through automated validation against the approved template.
```mermaid
graph TD
subgraph VERIFICATION["During Tool-Based Verification"]
TOOL_CFG["PCR Specification + EPD Template"]
REF["Reference EPD Generated by tool provider"]
APPROVE["Reference EPD Verified"]
TOOL_CFG --> REF
REF --> APPROVE
end
subgraph PRODUCTION["During Production"]
TEMPLATE["Verified EPD Template"]
V1["Variant EPD -Product A"]
V2["Variant EPD -Product B"]
V3["Variant EPD -Product C"]
TEMPLATE --> V1
TEMPLATE --> V2
TEMPLATE --> V3
end
APPROVE -->|"enables"| TEMPLATE
style VERIFICATION fill:#EC489922,stroke:#DB2777,stroke-width:2px,color:#DB2777
style PRODUCTION fill:#10B98122,stroke:#059669,stroke-width:2px,color:#059669
style TOOL_CFG fill:#F59E0B,color:#fff,stroke:#D97706
style REF fill:#8B5CF6,color:#fff,stroke:#7C3AED
style APPROVE fill:#10B981,color:#fff,stroke:#059669
style TEMPLATE fill:#EF4444,color:#fff,stroke:#DC2626
style V1 fill:#06B6D4,color:#fff,stroke:#0891B2
style V2 fill:#06B6D4,color:#fff,stroke:#0891B2
style V3 fill:#06B6D4,color:#fff,stroke:#0891B2
```
This is the core efficiency gain of tool-based verification: **verify once at the Category and Project levels, then produce many compliant variant EPDs without per-EPD review overhead.**
### Comparing the two approaches
| | Traditional (Manual Category) | Reference & Variant (Tool-Based Category) |
|---|---|---|
| **Verification target** | Each individual EPD | The tool + EPD Template (once per Category) |
| **Verifier involvement** | Reviews every EPD | Reviews tool & reference EPD(s); variants auto-validated |
| **Scalability** | Linear: cost grows with each EPD | High: verify once, produce many |
| **EPD types** | Standard EPDs only | Reference EPDs (verification) + Variant EPDs (production) |
| **Best for** | One-off assessments, low volume | High-volume product lines, tool providers, automated pipelines |
## What's next
- [Terminology](/concepts/terminology) -Full glossary of platform concepts including specifications, templates, and constraint types
- [EPD Templating](/concepts/epd-templating) -How templates enforce value ranges and validation constraints
---
Source: https://docs.smartepd.com/getting-started/authentication.md
# Authentication & Access
Smart EPD supports multiple authentication methods and a layered permission model that controls access at every level of the platform hierarchy.
## Authentication methods
| Method | Use case | How it works |
|--------|----------|-------------|
| **Email code** | Interactive login (UI and API) | Request a 6-digit code sent to your email, then exchange it for a JWT |
| **JWT Bearer token** | API requests | Include `Authorization: Bearer ` on every request. Tokens expire after 100 hours. |
| **API Key** | Automated integrations | Create a long-lived key (`sk_...`) via the API. Scoped to `read_only` or `read_write`. |
| **Session cookie** | Admin panel | Standard cookie-based auth |
| **None** | Public EPD library | `GET /api/v2/public/epds/` and the public EPD view are anonymous: send no credentials. See [Public EPD library](#public-epd-library). |
### Login flow
```mermaid
sequenceDiagram
participant User
participant SmartEPD as Smart EPD
participant Email
User->>SmartEPD: Request login code (email)
SmartEPD->>Email: Send 6-digit code
Email->>User: Code arrives
User->>SmartEPD: Submit code
SmartEPD->>User: JWT access token + user profile
```
### API Keys
For automated integrations (tool providers, CI/CD pipelines, data sync), API keys provide stable, long-lived authentication:
- Created via the API (requires JWT auth)
- Prefixed with `sk_` for easy identification
- Scoped to either **read-only** or **read-write**
- The raw key is shown **once** on creation; store it securely
Use API keys in the same `Authorization: Bearer sk_...` header format as JWTs.
### Public EPD library
Published EPDs are public content, so the endpoints that serve them need no authentication:
- `GET /api/v2/public/epds/` lists every publicly listed EPD across both Smart EPD platforms.
- `GET /api/v2/public/epds/{id}/view/` returns one EPD's full digital declaration.
They only ever return published, unexpired EPDs. An unpublished or expired EPD returns `404`, exactly like an id that doesn't exist, so nothing about a draft can be learned from them. Sending credentials is harmless but changes nothing. See the [Public EPD Library guide](/guides/public-epd-library).
## Permission model
Smart EPD uses a layered permission model where access is determined by your organization type, workspace role, and project membership.
### Organization level
Every user belongs to exactly one organization. Organization-level roles:
| Role | Permissions |
|------|------------|
| **Admin** | Full control: manage members, settings, billing |
| **Member** | Standard access: participate in workspaces and projects |
### Workspace level
When an organization joins a workspace, all its members receive baseline access. Individual users can be elevated:
| Role | Permissions |
|------|------------|
| **Admin** | Manage workspace members, invites, and member roles |
| **Editor** | Create and modify projects, EPDs, and submittals |
| **Viewer** | Read-only access to all workspace content |
:::note Workspace and Category administration
Creating, editing, archiving, or deleting **workspaces** and **Categories (PCCs)** is a platform-administration action, separate from the workspace roles above. In production these operations are performed by SmartEPD platform admins — contact support to request a change. Outside production, admins of the workspace's owning organization can also perform them. Project creation is not affected: workspace editors and admins can always create projects.
:::
### Organization role in workspace
Each organization has a role within the workspace that determines what actions are available:
| Org Role | Typical actions |
|----------|----------------|
| **Reporting Organization** | Owns projects, creates EPDs, submits for verification |
| **Tool Provider** | Registers tools in Categories, defines specifications, responds to verification |
| **Consultant** | Performs LCA work and EPD creation on behalf of manufacturers |
| **Verifier** | Reviews and certifies specifications, templates, and EPDs |
### Project level
Individual users are added to project teams with specific permissions:
| Permission | Effect |
|-----------|--------|
| **Can edit EPDs** | User can create and modify EPD content |
| **Team member** | User can view project content and participate in chat |
## Access inheritance
Permissions cascade downward through the hierarchy:
```
Organization membership
└── Workspace membership (viewer by default for all org members)
└── Category access (inherited from workspace)
└── Project team membership (explicit assignment)
```
A user must have organization membership *and* workspace access *and* project team membership to edit EPD data. This ensures that access is always scoped and traceable.
## Related pages
- [Platform Overview](/getting-started/platform-overview) -The entity hierarchy that permissions follow
- [Onboarding & Setup](/workflows/onboarding-workflow) -How organizations and workspaces are created
- [Data Model](/concepts/data-model) -Technical entity reference including membership models
---
Source: https://docs.smartepd.com/concepts/terminology.md
# Smart EPD Terminology
This page defines the shared terminology used across the Smart EPD platform. All role-specific guides reference these definitions as common language.
---
## LCA Platform
An **LCA Platform** is the underlying software environment used to run LCA calculations (e.g., SimaPro, GaBi, openLCA). Platforms go through a **platform pre-verification** process in the background, which — when successful — results in the platform being whitelisted for organizations to select when configuring Categories.
A platform is **not** the same as a background database (like ecoinvent). Databases are data sources consumed by platforms and tools.
## EPD Tool (Model)
An **EPD Tool** (sometimes called a "model") is a specific LCA model or configuration built on top of a platform. Tools are owned by tool-provider organizations and are selected at the Category level to streamline EPD production. A single platform can support many different tools across product categories.
## Platform Pre-Verification
**Platform pre-verification** is a background conformity review that assesses whether a platform's calculation framework, data handling rules, and reporting logic are fit to support compliant EPD development. When a platform passes pre-verification, it is whitelisted and available for organizations to select when configuring tool-based Categories. This is a one-time onboarding step.
## Tool/Model Verification
The process by which a **verifier** reviews and certifies that a specific tool/model produces compliant results for a specific project. While the tool is *selected* at the Category level, actual verification happens at the **project level** — tied to the LCA submission and EPD template review. The verifier reviews the LCA documentation together with a fully populated Reference EPD to confirm the model produces valid results within the verified scope. Because a template cannot be verified in the abstract, the template and its Reference EPD are verified in tandem: confirming the Reference EPD simultaneously validates the locked and inherited fields, the input bounds, and that a complete EPD comes out correct.
---
## Specifications & EPD Templates
A **Specification** is a structured definition of rules, requirements, and form configurations that govern how EPD data is collected, validated, and presented. Specifications exist at multiple levels and cascade downward, with each level inheriting from and optionally overriding the level above.
An **EPD Template** is the resolved output of the full specification chain for a specific project. It is not a separate concept from specifications; it is what you get when the entire chain is evaluated into a concrete form definition with locked fields, unlocked fields, and validation constraints.
Specifications attach at two levels:
```mermaid
graph TD
STD["Standard Specification ISO 21930 / EN 15804 Baseline rules & requirements"]
PCR_SPEC["PCR Specification EN 15804+A2 for Concrete Inherits standard + adds category rules"]
TEMPLATE["EPD Template The EPD form Locked/unlocked fields, validation"]
EPD_OUT["EPD Concrete product X Fully realized values"]
STD -->|"inherits & may override"| PCR_SPEC
PCR_SPEC -->|"resolves into"| TEMPLATE
TEMPLATE -->|"applied to"| EPD_OUT
style STD fill:#6366F1,color:#fff,stroke:#4F46E5
style PCR_SPEC fill:#8B5CF6,color:#fff,stroke:#7C3AED
style TEMPLATE fill:#EF4444,color:#fff,stroke:#DC2626
style EPD_OUT fill:#10B981,color:#fff,stroke:#059669
```
EPD Templates follow **semantic versioning**:
- **Minor updates** (1.0 → 1.1) auto-apply to all projects in the Category
- **Major updates** (1.0 → 2.0) do not auto-apply; existing projects stay on their current version
---
## Template Versioning & Tool Updates
Template constraints are not static. As tools evolve and product ranges expand, templates are updated through revision requests. Conceptually, tool updates fall into three categories of change:
```mermaid
graph LR
subgraph TYPE0["Type 0 -Automated"]
T0["Background data updates e.g., new ecoinvent version
No verifier review"]
end
subgraph TYPE1["Type 1 -Scoped Review"]
T1["Minor functional changes e.g., new product subcategory
Verifier reviews delta only"]
end
subgraph TYPE2["Type 2 -Full Re-Verification"]
T2["Major methodology changes e.g., fundamental model restructure
Full re-verification required"]
end
style TYPE0 fill:#10B98122,stroke:#059669,stroke-width:2px,color:#059669
style TYPE1 fill:#F59E0B22,stroke:#D97706,stroke-width:2px,color:#D97706
style TYPE2 fill:#EF444422,stroke:#DC2626,stroke-width:2px,color:#DC2626
style T0 fill:#10B981,color:#fff,stroke:#059669
style T1 fill:#F59E0B,color:#fff,stroke:#D97706
style T2 fill:#EF4444,color:#fff,stroke:#DC2626
```
| Change | Update Type | Template Version | Effect on Existing Projects |
|---|---|---|-----------------------------|
| Background database update | Type 0 | No change | No effect |
| Adjusted constraint range | Type 1 | Minor bump (1.0 → 1.1) | Auto-applies to new EPDs |
| New constraint type added | Type 1 | Minor bump (1.0 → 1.1) | Auto-applies to new EPDs |
| Fundamental methodology change | Type 2 | Major bump (1.x → 2.0) | Does not auto-apply |
---
## Related pages
- [Platform Overview](/getting-started/platform-overview) -Entity hierarchy and EPD workflow overview
- [Tool Verification & Constraints](/guides/tool-verification-and-constraints) -Why EPD tools are verified and how constraints enforce the verified envelope
- [EPD Templating](/concepts/epd-templating) -Deep dive into template constraints and out-of-bounds handling
- [Specifications & Validation](/concepts/specs-and-validation) -How the specification engine works
---
Source: https://docs.smartepd.com/concepts/data-model.md
# Data Model
This page maps the Smart EPD entity hierarchy and explains how the core objects relate to each other. Understanding this model is essential for API integrators and anyone working with Smart EPD data programmatically.
## Entity hierarchy
Smart EPD organizes data through a strict containment hierarchy. Each level constrains the next:
```mermaid
erDiagram
Organization ||--o{ WorkspaceMembership : "joins via"
Organization ||--o{ Tool : "owns"
Workspace ||--o{ WorkspaceMembership : "has"
Workspace ||--o{ PCC : "contains"
PCC }o--o| Spec : "governed by"
PCC }o--o| Tool : "selects"
PCC ||--o{ Project : "contains"
Project ||--o{ ResolvedTemplate : "resolves into"
Project ||--o{ EPD : "contains"
Project ||--o{ LCAVersion : "contains"
Project ||--o{ SubmittalPackage : "contains"
ResolvedTemplate ||--o{ EPD : "governs reference/variant"
EPD ||--o{ VariantEpd : "spawns variants"
EPD ||--o{ VerifierAssignment : "reviewed by"
LCAVersion ||--o{ LCADocument : "has"
LCAVersion ||--o{ VerifierAssignment : "reviewed by"
SubmittalPackage }o--|| LCAVersion : "bundles LCA"
SubmittalPackage ||--o{ SubmittalItem : "contains"
SubmittalItem }o--o| EPD : "reviews"
SubmittalItem }o--o| LCAVersion : "reviews"
Spec ||--o{ SpecVersion : "versioned as"
```
### EPD type progression
EPDs under a project can follow one of two paths. Every EPD begins life as a `standard` draft. From there, it can be submitted and approved on its own (standalone path), *or* it can be morphed into a `reference` EPD, which locks a modeling envelope and then serves as the parent for zero or more `variant` EPDs.
```mermaid
graph LR
NEW([New EPD]) --> STD[Standard Draft]
STD -->|submit and approve| STDP[Standard EPD standalone]
STD -->|morph to reference while still in draft| REF[Reference Draft]
REF -->|submit and approve requires template| REFP[Reference EPD populated instance]
REFP -->|generate variant| VAR[Variant EPD inherits template]
style NEW fill:#6366F1,color:#fff,stroke:#4F46E5
style STD fill:#E0E7FF,stroke:#6366F1,color:#4F46E5
style STDP fill:#10B981,color:#fff,stroke:#059669
style REF fill:#E0E7FF,stroke:#6366F1,color:#4F46E5
style REFP fill:#8B5CF6,color:#fff,stroke:#7C3AED
style VAR fill:#EC4899,color:#fff,stroke:#DB2777
```
The standalone and reference paths are mutually exclusive per EPD: once an EPD is morphed to `reference`, it cannot become `standard` again, and variants always belong to exactly one reference.
## Platform layer
### Organization
A company or entity on the platform. Every user belongs to exactly one organization, and every action traces back to one.
| Attribute | Description |
|-----------|-------------|
| **Name** | Company name |
| **Type** | `manufacturing_group`, `tool_provider`, `consultant`, or `verifier` |
| **Members** | Users with roles (`admin` or `user`) |
| **Parent org** | Self-referential link for corporate hierarchy (e.g., subsidiaries under a parent group) |
| **Partner orgs** | Explicit one-way links to other organizations for collaboration, tracked via a `PartnerOrganization` record with its own `active` / `revoked` status |
Organizations can also receive scoped `OrgAccessGrant` records, which grant a partner organization access to a specific workspace, PCC, or project.
### Workspace
A collaboration space where organizations work together. One workspace per engagement.
| Attribute | Description |
|-----------|-------------|
| **Name** | Engagement name (e.g., "Acme 2025 EPDs") |
| **Primary organization** | The reporting org this workspace was created for |
| **Status** | `draft` → `active` → `archived` |
| **Organization memberships** | Each org has a role: `reporting_organization`, `tool_provider`, or `member` |
| **User memberships** | Individual users get workspace-level roles: `admin`, `editor`, or `viewer` |
| **PCCs** | Product Category Configurations within this workspace |
When an organization joins a workspace, all of its current members automatically receive viewer-level access. Users added to the org later are not auto-enrolled — they must be invited explicitly. Individual users can be elevated to `editor` or `admin`.
:::note
Consultant and verifier relationships are expressed at the **organization type** level (`org_type`), not as workspace roles. Workspace membership only distinguishes the *reporting* org, the *tool provider*, and general *members* for access-control purposes.
:::
### PCC (Product Category Configuration)
The governance layer, defining how a product category's EPDs are created and verified. Exposed in the UI as a "Category" and in frontend routes as `/category/:id`, but the backend model is named `PCC`.
| Attribute | Description |
|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Name** | Product category label (e.g., "Ready-Mixed Concrete") |
| **PCR Spec** | Which Product Category Rules specification governs this PCC |
| **Tool** | For tool-based modes, the selected LCA tool. The tool's `allows_reference_epds` flag determines whether projects under this PCC can create reference/variant EPDs. |
| **Tool version** | Version string of the selected tool |
| **Status** | `draft` → `active` → `archived` |
## Product layer
### Project
A concrete product assessment. The central hub for all EPD work.
| Attribute | Description |
|-----------|-------------|
| **Name** | Product being assessed |
| **PCC** | Parent Product Category Configuration (governs rules, tool, verifier) |
| **Status** | `draft` → `retired` |
| **Sequence ID** | Auto-incremented identifier assigned on first EPD publish, used in declaration numbers |
| **Reporting owner metadata** | Optional declaration-owner details (name, phone, address, website, email) captured at the project level |
| **Team** | Individual users with per-project permissions |
| **Revision chain** | Self-referential link (`previous_revision`) pointing to a prior project. A unique constraint ensures each revision has at most one successor. |
A project contains EPDs, LCA versions, zero or more `ResolvedTemplate` instances (one per approved spec version that applies here), and submittal packages.
### EPD (Environmental Product Declaration)
The core deliverable: a structured environmental impact report for a product. Every EPD stores its content in a `document` JSONField — a structured blob keyed by the project's resolved spec.
**EPD types:**
| Type | Description |
|------|-------------|
| `standard` | Standalone EPD, individually reviewed. Does not use a locked template. |
| `reference` | A verification artifact bound to a locked `ResolvedTemplate`. The **template** defines the modeling envelope variants must stay within; the reference EPD is the populated instance that proves it. Produced by morphing a `standard` draft — see the [EPD type progression](#epd-type-progression) diagram above. |
| `variant` | Derived from an approved reference EPD via the `VariantEpd` junction. Inherits the reference's template and is validated against its constraints on generation. |
An EPD carries tracking metadata through its lifecycle: submitted_at/by, approved_at/by, flagged_at/by, published_at/by, declaration_number and expiration_date, and pdf_file_key/pdf_generated_at for the rendered PDF.
**Status lifecycle:**
```mermaid
stateDiagram-v2
[*] --> draft
draft --> submitted: submit
submitted --> in_review: start review
in_review --> approved: approve
in_review --> flagged: flag
flagged --> in_review: resolve
approved --> published: publish
published --> republishing: request republication
republishing --> published: complete republication
```
Variant-specific models layer on top of this: `OutlierFlag` tracks statistical outliers on variant indicator values, and `RepublishingRequest` (with child `RepublishingSection` records) drives the approval flow when a previously-published EPD needs to be updated.
### LCA Version
Life Cycle Assessment documentation for a project. The LCA must be approved before EPDs can be approved.
**Status lifecycle:** `draft` → `submitted` → `in_review` → `approved` (or `rejected` → `reopened`)
LCA versions contain uploaded documents staged as either LCA submittal documents or supporting documentation. Each `LCADocument` also carries a `document_type` categorization (LCA report, supporting data, data-quality assessment, methodology notes, other) and supports soft deprecation via `deprecated_at` / `deprecated_by` rather than hard deletion, preserving the audit trail. LCA versions also track submission and review metadata (`submitted_by`, `reviewed_by`, `reviewed_at`, `review_notes`) and an optional `tool_url` for linking to the external LCA tool.
## Specification & Template layer
### Specification (Spec)
A structured definition of rules, requirements, and form configurations. Specifications exist at two levels and cascade downward through inheritance:
| Level | Example | What it defines |
|-------|---------|-----------------|
| **Standard** | ISO 21930 / EN 15804 | Baseline structural requirements |
| **PCR** | EN 15804+A2 for Concrete | Category-specific rules, impact categories, input constraints, field configurations |
Specifications are **versioned** using semantic versioning. Each version contains requirements (with validation checks), field group definitions, and section structures.
For a deep dive, see [Specifications & Validation](/concepts/specs-and-validation).
### EPD Template
The output when the full specification chain is evaluated for a specific project. Backed by the `ResolvedTemplate` model. An EPD Template locks field values, applies constraints, and defines the form that manufacturers interact with when authoring EPDs that are bound to it.
**Status lifecycle:** `draft` → `in_review` → `approved` (or `rejected`)
Templates are immutable after approval. Template versions are tracked via semantic versioning: minor updates auto-apply to existing projects, major updates do not.
**How each EPD type uses (or doesn't use) a template:**
| EPD type | Template relationship | What the template does |
|----------|----------------------|------------------------|
| `standard` | No template required (`template` FK is null) | The EPD is authored free-form against the PCC's spec. Validation comes from the spec itself, not from a locked envelope. |
| `reference` | **Required** — a `ResolvedTemplate` is auto-created when a standard draft is morphed to reference | Locks the verified modeling envelope (locked field values + constraint ranges). The reference EPD proves the tool-and-spec combination works at these setpoints. |
| `variant` | Inherits the template from its parent reference EPD (via the `VariantEpd` junction) | Every variant is validated against the same constraints the reference was approved under. Out-of-bounds inputs block variant generation — see [EPD Templating](/concepts/epd-templating) for the full out-of-bounds flow. |
In short: standard EPDs are the free-form path, and reference/variant EPDs are the templated path. A project can contain both — standard EPDs for one-off products, reference EPDs (each with its own variant family) for tool-driven product lines.
For a deep dive, see [EPD Templating](/concepts/epd-templating).
## Verification layer
### Submittal Package
A bundle submitted by a manufacturer for third-party verification. Groups together the items a verifier needs to review as a unit.
**Status lifecycle:** `draft` → `submitted` (locked) → `in_review` → `approved` (or `rejected` / `feedback`)
Each package contains **submittal items**, each independently reviewable:
| Item type | Review scope |
|-----------|-------------|
| LCA version | LCA documentation completeness |
| Standard EPD | Full section-by-section review |
| Reference EPD | Template + tool compliance review |
| Variant EPD | Automated validation + spot checks |
**Dependency gates** enforce approval order: LCA → EPDs → Reference EPD → Variant EPDs.
### Verification mechanics
Verification assignments are created per-EPD, per-PCF, or per-LCA version — not at the Category or project level. The verification system then operates at the section level within each assigned item:
| Component | Purpose |
|-----------|---------|
| **Verifier assignments** | Per-EPD, per-PCF, or per-LCA, with status tracking |
| **Verification comments** | Per-section threaded discussions with author replies |
| **AI conformance hints** | Automated suggestions with confidence levels |
| **Verifier checklists** | Per-requirement compliance tracking (Y/N/OFI/N/A) |
For the full verification process, see [Verification Workflow](/workflows/verification-workflow).
## Related pages
- [Platform Overview](/getting-started/platform-overview) -Visual introduction to the entity hierarchy
- [Terminology](/concepts/terminology) -Definitions for all platform concepts
- [Specifications & Validation](/concepts/specs-and-validation) -How the specification inheritance chain works
- [EPD Lifecycle](/workflows/epd-lifecycle) -How entities move through the status lifecycle
---
Source: https://docs.smartepd.com/concepts/epd-schema.md
# EPD Schema (API)
Once an EPD has been created, the **schema endpoint** tells you exactly which fields it expects, which requirements will be checked against it, and what constraints apply. Use it to drive dynamic forms, build validation in your own integrations, or simply discover what a given EPD's spec looks like.
This page is the field-by-field reference for the response. For the *concepts* behind templates, locked fields, and constraints, see [EPD Templating](/concepts/epd-templating) and [Specs and Validation](/concepts/specs-and-validation).
## Endpoint
```
GET /api/v2/epds/{epd_id}/schema/?view={full|summary}
```
| Query param | Default | Effect |
|---|---|---|
| `view=full` | ✅ | Returns the verbose internal shape. Stable across releases but carries inheritance metadata, legacy duals, and resolver-internal flags. |
| `view=summary` | | Returns a slim, integrator-friendly shape. Recommended for new integrations and dynamic form generation. |
The endpoint walks the EPD's pinned spec inheritance chain (PCR → standard → category-specific overrides) and emits the fully resolved set of input fields and requirements. Requirements are returned **without their internal `checks`** — those are evaluated server-side by the validation engine.
:::note Removed items are omitted
The schema only ever contains fields and requirements that are **live** for this EPD's spec. When a PCR override removes an inherited field or requirement, it does **not** appear in either view — there is no `_removed` flag in the public response. (Internally the resolver marks removed items so the spec editor can render them struck-through, but that marker is never exposed here.) Treat the schema as authoritative: anything present is collected; anything removed is simply absent.
:::
## Two views at a glance
| | `view=full` | `view=summary` |
|---|---|---|
| Top-level keys | `input_field_defs`, `requirements`, `indicator_catalog` | `fields`, `requirements`, `counts` |
| Field key style | `field_id`, `is_required`, `group_id`, `show_if_json` | `id`, `required`, `group`, `show_if` |
| Inheritance metadata | Included (`_inherited`, `_inherited_from`, `override_mode`) | Stripped |
| Legacy duals | Included (`constraints_json`, scalar `cross_ref_id`) | Stripped (only normalized form remains) |
| Best for | Internal tooling, debugging the resolver | API integrators, form builders, third-party validators |
:::tip
If you're starting fresh, use `?view=summary`. The full view exists for backwards compatibility and parity with the resolver's internal shape.
:::
---
## Summary view reference
```bash
curl -H "Authorization: Bearer $TOKEN" \
"https://sandbox.smartepd.xyz/api/v2/epds/$EPD_ID/schema/?view=summary"
```
Response shape:
```json
{
"fields": [ /* EpdSchemaSummaryFieldOut */ ],
"requirements": [ /* EpdSchemaSummaryRequirementOut */ ],
"counts": { "fields": 47, "requirements": 23 }
}
```
### `fields[]`
Each field describes one input on the EPD form.
| Key | Type | Meaning |
|---|---|---|
| `id` | string | Stable field identifier. Use this as the document key when filling the EPD (e.g., `product-name`, `a1_a3_gwp`). |
| `type` | string | Input type: `text`, `number`, `date`, `select`, `multiselect`, `richtext`, `table`, `plant_picker`, `ec3_category`, etc. Drives which form widget to render. |
| `label` | string | Human-readable label for the form field. |
| `help_text` | string | Hint shown beneath the input. May be empty. |
| `group` | string \| null | The `id` of the [field group](#groups) this field belongs to, or `null` if ungrouped. Use to render related fields together. |
| `order` | int | Display order within the group (or globally, if ungrouped). Sort ascending. |
| `required` | bool | If `true`, the EPD will fail submission unless this field has a value. |
| `locked` | bool | If `true`, the value is prefilled by the template and cannot be edited. Render as read-only and use `locked_value` for the displayed value. |
| `locked_value` | any \| null | The prefilled value when `locked` is `true`. Type matches `type` (string, number, list, etc.). |
| `options` | list \| null | For `select` and `multiselect` types: the available choices, each as `{label, value, ...}`. `null` for free-text fields. |
| `constraints` | list | Validation rules attached to this field. See [Constraint shapes](#constraint-shapes). |
| `show_if` | object \| null | Conditional visibility rule. The field only renders when the rule evaluates to true against the current document values. See [Conditional logic](#conditional-logic). |
| `default` | any \| null | Default value to seed the input with on first render. |
### `requirements[]`
Requirements are compliance clauses (typically from the PCR) that the EPD must satisfy. The schema endpoint exposes them so integrators can present them alongside the form, but **the actual evaluation happens server-side** when the EPD is submitted (`/epds/{id}/submit/`) or validated (`/epds/{id}/validate/`).
| Key | Type | Meaning |
|---|---|---|
| `id` | string | Stable requirement identifier (e.g., `EN15804_4.1`). |
| `title` | string | Short heading. |
| `text` | string | Full requirement text — show this to verifiers and manufacturers. |
| `topic` | string | Logical grouping (e.g., `lifecycle_stages`, `declared_unit`). Use to organize the verifier sidebar. |
| `comment_type` | string | One of `mandatory`, `guidance`, `info`. Drives severity in the UI. |
| `applies_to` | list<string> | Document types this requirement applies to: usually `["epd"]`, sometimes `["lca_report"]` or `["pcr"]`. |
| `refs` | list<string> | The `field.id` values this requirement cross-references. Use to link a requirement back to the form fields it governs. |
| `applies_if` | object \| null | Conditional applicability — a **document-text search** gate (`{"document_type", "query", "mode"}` plus an optional boolean `negate`; modes: `all_phrases`, `any_phrases`, `any_topics`, `regex`), evaluated against the submitted documents. **Not** the `show_if` shape — see [Conditional logic](#conditional-logic). |
### `counts`
A small object with `{"fields": N, "requirements": M}`. Useful for quick sanity checks ("does this template have the expected number of fields?") without iterating the lists.
---
## Constraint shapes
Each item in `field.constraints` has this shape:
```json
{
"constraint_type": "range",
"parameters_json": { "min": 0, "max": 1000, "bounds_mode": "inclusive" },
"severity": "error",
"applies_if_json": null
}
```
| Key | Meaning |
|---|---|
| `constraint_type` | One of `range`, `statistical`, `formula`, `allowed_values`. |
| `parameters_json` | Type-specific params — see [EPD Templating](/concepts/epd-templating#input-range-constraints) for full details. Keys per type: `min`/`max`/`bounds_mode` (range); `distribution`/`threshold` plus `mean`/`std_dev` for normal or `geometric_mean`/`geometric_std_dev` for lognormal (statistical); `expression`/`operator` with `threshold`, or `min`/`max` for the `between` operator (formula); `values`/`case_sensitive` (allowed_values). |
| `severity` | `error` blocks variant approve/auto-approve, and blocks a reference EPD's submission when the template has `constraints_enforced` on unless the author [acknowledges the failure](/guides/reference-epds-and-constraints#submitting-a-reference-epd). `warning` and `info` are advisory. |
| `applies_if_json` | Optional conditional gate — `{field, operator, value}` leaves with `all`/`any` combinators. See [Conditional logic](#conditional-logic) below. **Not** the `show_if` shape. |
Spec-level constraints anchor implicitly to the field that declares them. Template-level constraints (the template's `template_constraints` list) additionally carry a **required** `reference_field_id` — the anchor field that violations are attributed to — and a display `label`.
---
## Conditional logic
Three different conditional gates exist in the schema, and they do **not** share a shape — don't copy snippets between them.
**`show_if` (field visibility)** puts the operator **in the key**:
```json
{ "field": "product-form", "eq": "powder" }
```
Leaf operators: `eq`, `ne`, and `contains` (list membership, for multiselect fields). Compose with `any` / `all`:
```json
{
"all": [
{ "field": "country", "eq": "US" },
{ "field": "applicable-in", "contains": "EU" }
]
}
```
A `null` or non-object condition, and any unknown leaf operator, default to **visible**. Combinator values must be arrays of condition objects — other shapes are outside the contract. (Note that `{"any": []}` is well-formed and evaluates false: no branch matched, so the field is hidden.) Evaluate against the in-flight document values to decide whether to render the field; the server applies the same rules at submission time (hidden fields are not validated), so client-side evaluation is purely for UX.
**Constraint `applies_if_json`** uses `{field, operator, value}` leaves with operators `eq`, `neq`, `in`, `gt`, `gte`, `lt`, `lte`, `present`, `absent`, composed with `all` / `any` — see the [conditional application grammar](/concepts/epd-templating#conditional-application).
**Requirement `applies_if`** is a document-text search (`document_type` + `query` + `mode`), unrelated to either of the above.
---
## Groups
Field groups aren't returned as a separate top-level list in either view (they live in the spec, not the schema response). To render grouped forms, bucket `fields` by `group` yourself:
```bash
curl -H "Authorization: Bearer $TOKEN" \
"https://sandbox.smartepd.xyz/api/v2/epds/$EPD_ID/schema/?view=summary" \
| jq '.fields | group_by(.group) | map({group: .[0].group, fields: map(.id)})'
```
If you need group labels and ordering, fetch the spec version directly via `GET /api/v2/specs/{spec_id}/versions/{version}/`, which exposes `field_groups[]` with `id`, `label`, `description`, `order`, and nested `parent_group`.
---
## Full view reference
The full view is the verbose default. It carries everything the summary does, plus the keys in the table below. Most integrations should prefer `?view=summary`; reach for `view=full` only when you need the resolver's internal state (e.g., debugging an inheritance override).
### Extra keys on each `input_field_defs[]` entry
| Key | Meaning |
|---|---|
| `field_id` | Same as summary `id`. |
| `is_required` | Same as summary `required`. |
| `group_id` | Same as summary `group`. |
| `placeholder` | UI placeholder for the input. |
| `default_value` | Same as summary `default`. |
| `options_json` | Same as summary `options`. |
| `show_if_json` | Same as summary `show_if`. |
| `constraints_json` | **Legacy.** Inline constraint blob; superseded by the normalized `constraints[]` list. Prefer `constraints[]`. |
| `view_hidden` | If `true`, the field is collected during data entry but suppressed from the digital EPD view. |
| `_inherited` | `true` if the field came from a parent spec rather than the leaf spec. |
| `_inherited_from` | Display name of the parent spec, if inherited. |
| `override_mode` | How an inherited field was modified by a child spec: `replace`, `merge`, etc. |
### `indicator_catalog`
Full view only. The slice of the LCIA indicator catalog this EPD's catalog-mode
results table may draw rows from, or `null` when its spec declares no such
table. Shaped `{catalog_version, methods[], entries[]}` — the same payload
`GET /api/v2/specs/indicator-catalog/` returns, restricted to the methods the
table permits.
Because the summary view omits this key, integrations on `?view=summary` should
read the catalog from the standalone endpoint. See
[Writing Results Data](/guides/writing-results-data#reading-the-catalog) for the
entry shape and how the ids are used.
### Extra keys on each `requirements[]` entry
| Key | Meaning |
|---|---|
| `requirement_id` | Same as summary `id`. |
| `document_types` | Same as summary `applies_to`. |
| `cross_ref_ids` | Same as summary `refs`. |
| `cross_ref_id` | **Legacy.** Single scalar cross-reference held during the migration to `cross_ref_ids[]`. Prefer the list. |
| `output_targets` | Where the requirement appears in generated output (e.g., `["pdf", "digital"]`). |
| `sources_json` | Citations and external references (e.g., PCR section numbers). |
| `compliance_requirement` | `shall`, `should`, or `may`. |
| `applies_if_json` | Same as summary `applies_if`. |
| `inherited` | `true` if the requirement came from a parent spec. |
---
## Worked example: render a form from the schema
This jq snippet pulls the summary view and prints a flat outline grouped by section, with required fields marked:
```bash
curl -s -H "Authorization: Bearer $TOKEN" \
"https://sandbox.smartepd.xyz/api/v2/epds/$EPD_ID/schema/?view=summary" \
| jq -r '
.fields
| group_by(.group // "ungrouped")
| .[]
| "## \(.[0].group // "Ungrouped")\n" +
(map(
"- \(.id) [\(.type)]" +
(if .required then " *required*" else "" end) +
(if .locked then " (locked: \(.locked_value | tojson))" else "" end) +
(if (.constraints | length) > 0 then " — \(.constraints | length) constraint(s)" else "" end)
) | join("\n"))
'
```
For a richer integration, walk `fields` to build form widgets keyed by `type`, evaluate each `show_if` against the current document state, and render `constraints` inline as helper text or client-side validators. Then submit the populated document to `PUT /api/v2/epds/{epd_id}/document/` — the server re-evaluates everything authoritatively at `POST /api/v2/epds/{epd_id}/submit/`.
---
## Related
- [EPD Templating](/concepts/epd-templating) — what locked, unlocked, and constrained fields *mean* in the verification model.
- [Specs and Validation](/concepts/specs-and-validation) — how requirements, fields, and the inheritance chain are authored upstream.
- [API Quickstart, step 6](/guides/api-quickstart) — the end-to-end walkthrough where this endpoint first appears.
---
Source: https://docs.smartepd.com/concepts/specs-and-validation.md
# Specifications & Validation
The Smart EPD specification system is a declarative framework for defining validation rules, input forms, and compliance checks for EPDs and PCFs. A single specification encodes everything needed to validate a document, render an input form, and produce compliance reports, with inheritance allowing product-specific specifications to extend shared base specifications.
## The specification chain
Specifications exist at two levels, each inheriting from the one above:
```mermaid
graph TD
STD["Standard Specification e.g., ISO 21930 / EN 15804 Baseline rules shared across categories"]
PCR["PCR Specification e.g., EN 15804+A2 for Concrete Category-specific rules"]
TEMPLATE["EPD Template Locked/unlocked fields, validation, form Versioned and immutable after approval"]
STD -->|"inherits & may override"| PCR
PCR -->|"resolves into"| TEMPLATE
style STD fill:#6366F1,color:#fff,stroke:#4F46E5
style PCR fill:#8B5CF6,color:#fff,stroke:#7C3AED
style TEMPLATE fill:#EF4444,color:#fff,stroke:#DC2626
```
**Standard Specification** defines baseline rules that apply across product categories: structural requirements from ISO 21930 or EN 15804.
**PCR Specification** inherits from the standard and adds category-specific rules: which modules must be declared, what impact categories are required, product-specific reporting formats, input validation ranges, required fields, field types and formats, and visibility rules based on product type.
The **EPD Template** is what results when the full chain is evaluated for a specific project. It is not separately authored; it is the combined, flattened output of both levels.
## How inheritance works
Each specification level can:
- **Add** new requirements, fields, and constraints not present in the parent
- **Supersede** inherited items — a child requirement or field with the same ID replaces the parent's version entirely
- **Remove** inherited requirements or fields that don't apply (`remove_requirement`, `remove_field` overrides)
- **Amend** specific parameters of inherited items without full replacement (`amend_requirement`, `amend_field` overrides)
Inheritance is resolved recursively. A specification can have multiple parents (via `SpecInheritanceLink` records with merge ordering). During resolution, parent specs are merged depth-first; child items supersede parent items on duplicate IDs, and unmatched parent items are preserved. Configuration blobs (bindings, normalizers, AI context, conventions) are deep-merged.
This means a concrete PCR specification doesn't need to redefine everything from scratch. It inherits the standard's structural requirements and only specifies what's different for concrete products.
### Versioning
Specifications follow semantic versioning:
- **Patch** (1.0.0 → 1.0.1) -Minimal, non-breaking updates to the spec.
- **Minor** (1.0 → 1.1) -Minor changes to the spec like requirement adjustments, new fields.
- **Major** (1.x → 2.0) -Fundamental methodology changes. Does **not** auto-apply; existing projects stay on their current version.
## What specifications define
A specification contains several types of content:
### Requirements (validation rules)
Each requirement defines one or more checks that evaluate whether EPD data meets a specific criterion. Requirements have:
- A **type**: direct (maps to a standard clause), derived (inferred from multiple clauses), or contextual (depends on product context)
- A **severity**: error (must fix before publication), warning (should fix, not blocking), or info (informational)
- One or more **checks**: the actual validation logic
### Conditional guards
Checks can be conditionally applied: they only run when certain conditions are met. This handles product configurations where validation rules vary by variant:
*Example: "If product type is concrete, compressive strength must be declared" -the check for compressive strength only runs when the product type field equals "concrete".*
### Spec parameters (instantiation-time flags)
Conditional guards react to *document values* — answers the manufacturer has already given. Spec parameters gate content on facts known **before any document exists**, decided when the spec is instantiated for an EPD.
A specification declares its parameters (each a boolean with a default, e.g. `tool: false`), and any requirement, input field, or field group can opt in with an `include_if_params` gate. When an EPD is created, the platform binds concrete values — the `tool` parameter follows the project's **Is a Tool** flag (the single, owner-editable source of truth for whether a project's EPDs come from a pre-verified tool), and admins can layer additional per-category overrides on top — then snapshots them onto the EPD alongside its pinned spec version. Everything downstream (the input form, document validation, the verifier checklist, AI conformance evaluation, and the rendered PDF) resolves through that binding, so a gated-off question or requirement simply does not exist for that EPD.
*Example: the Smart EPD GPI declares a `tool` parameter. Its EPD Tool requirements (§6.12 — methodology report, representative-EPD verification, tool documentation, validity period, change log) are gated `tool: true`, so they only appear on checklists for EPDs in projects flagged as tools.*
Parameters merge through the inheritance chain like other content (child redeclarations win), and the binding is editable while the EPD is a draft, frozen from submission onward.
### Input fields (form definitions)
Specifications also define input forms, which are the fields that EPD authors fill in. Each field has a type (text, number, select, date, file, etc.), validation rules, and conditional display logic (show/hide based on other field values).
Fields are organized into collapsible groups for a clean editing experience. When the specification chain resolves, the form is what manufacturers interact with to create EPDs.
## Cross references
Each requirement may reference one or more input fields via `cross_ref_ids` (an ordered list). Cross references let specs link a requirement clause directly to the field(s) that satisfy it.
---
## Related pages
- [Terminology](/concepts/terminology) -Platform concepts and constraint types reference
- [EPD Templating](/concepts/epd-templating) -How templates enforce constraints and handle out-of-bounds inputs
---
Source: https://docs.smartepd.com/concepts/epd-templating.md
# EPD Templating
EPD Templates define the **verified modeling envelope** for a given tool within a project. They validate performance at defined boundary conditions and confirm that the tool enforces approved limits without exceeding the verified scope.
:::note Key principle
Smart EPD provides the framework and logic for defining, enforcing, and versioning template constraints. The specific setpoints, ranges, and rules within a template are determined by tool providers, manufacturers, consultants, and verifiers during the verification process. Smart EPD facilitates; it does not prescribe.
:::
## How templates relate to specifications
A template is not authored directly. It is the **resolved output** of the specification inheritance chain. Each level can define or override constraints, and the template is what results when the full chain is evaluated:
```mermaid
graph TD
STD["Standard Specification Baseline rules & requirements e.g., ISO 21930 / EN 15804"]
PCR["PCR Specification Category-specific rules e.g., EN 15804+A2 for Pipes"]
VERIFIER["Verifier Review Approves or adjusts constraints"]
TEMPLATE["EPD Template Locked + unlocked fields Versioned, immutable after approval"]
STD -->|"inherits"| PCR
PCR -->|"reviewed by"| VERIFIER
VERIFIER -->|"resolves into"| TEMPLATE
style STD fill:#6366F1,color:#fff,stroke:#4F46E5
style PCR fill:#8B5CF6,color:#fff,stroke:#7C3AED
style VERIFIER fill:#EC4899,color:#fff,stroke:#DB2777
style TEMPLATE fill:#EF4444,color:#fff,stroke:#DC2626
```
The specification chain defines the rules. The verifier reviews, adjusts, and approves the constraints. The result is a **locked, versioned EPD Template** that governs every EPD created within that project.
Within an EPD Template, each field has a defined state:
- **Locked fields** -Prefilled values that cannot be changed (e.g., declared unit, methodology references)
- **Unlocked fields** -Editable by the project team, but subject to validation constraints
- **Constrained fields** -Unlocked, but bounded by range constraints
---
## Templates and Reference EPDs
A template defines *what is allowed* — locked values and input bounds — but it is **not** itself a declaration, and it cannot be verified in the abstract: there is nothing concrete to check until real values exist. Supplying those values is the job of a **[Reference EPD](/guides/reference-epds-and-constraints)** — the fully populated instance that fills in every field and anchors a product family.
The template and its Reference EPD are therefore **verified together, in a single pass**. Approving the Reference EPD simultaneously confirms the template's locked fields, its input bounds, and that a complete, conformant EPD comes out the other side. Once approved, the template governs every variant derived from that reference.
In short: the **template** is the rulebook and the form; the **Reference EPD** is that form filled with representative numbers to prove the rulebook works; **variant EPDs** are validated against the same template constraints when they are generated.
---
## Template characteristics
Each EPD Template represents a **bounded modeling framework**. The dimensions along which a template defines its scope are flexible and project-specific, but common examples include:
### Manufacturing plants or plant archetypes
The template may scope which manufacturing facilities are covered. A product manufactured at Plant A under different energy mixes than Plant B may require separate templates or plant-level parameterization.
### Manufacturing process archetypes
The template may define which production processes are in scope, such as extrusion, injection molding, or assembly. Different processes may produce different environmental profiles even for the same product family.
### Material families
The template may bound which material families are permitted, such as PE, PVC, or PP for a plastics manufacturer. Materials outside the defined families fall outside the verified scope.
### Parameter ranges (input constraints)
The most nuanced dimension. Parameter ranges define the **allowable variability** for inputs that drive the LCA model: dimensions, material composition, energy use, waste ratios. These ensure variant EPDs stay within the verified modeling envelope.
:::tip
The dimensions listed above are illustrative examples. EPD Templating is flexible: a template can define scope along any dimension relevant to the product category and verification context. The platform does not prescribe a fixed set of bounding dimensions.
:::
---
## Input range constraints
Smart EPD supports four constraint types for validating input values: `range`, `statistical`, `formula`, and `allowed_values`. These are **not mutually exclusive**: a single template can combine several to create a comprehensive validation framework. The verifier determines which types to apply and what specific values to set.
Any constraint can also be **conditional** via the `applies_if_json` field — the constraint is only evaluated when the specified condition is true.
### Range constraints
Numeric min/max bounds on an input value with configurable inclusivity (inclusive, exclusive, left-open, right-open). Catches data entry errors and enforces verified scope boundaries. Most templates will have range constraints on the majority of their inputs.
*Example: Total product mass must be between 0.5 and 25 kg. PE content must fall within 0.30–0.78 kg PE / kg product.*
Range constraints handle absolute bounds, ratio-based bounds, and any simple min/max pattern. The `parameters_json` keys are `min`, `max` (either may be omitted for an open-ended bound), and `bounds_mode` — one of `inclusive` (default), `exclusive`, `left_open`, or `right_open`.
```json
{ "min": 0.5, "max": 25, "bounds_mode": "inclusive" }
```
### Statistical constraints
Distribution-based bounds using z-score checks. Values outside a defined statistical threshold from the population mean are flagged as outliers. Supports both normal and lognormal distributions.
*Example: GWP value more than 2 standard deviations from the category mean is flagged.*
The `parameters_json` keys are `distribution` (`"normal"`, the default, or `"lognormal"`) and `threshold` — the maximum allowed z-score (default `3.0`). A normal distribution takes `mean` and `std_dev`; a lognormal distribution takes `geometric_mean` (> 0) and `geometric_std_dev` (> 1) instead.
```json
{ "distribution": "normal", "mean": 50, "std_dev": 10, "threshold": 2 }
```
Most valuable for mature product categories with many existing declarations.
### Formula constraints
Constraints defined by an **arbitrary mathematical expression** evaluated against EPD data. Where range constraints handle simple bounds, formula constraints handle relationships across multiple fields — non-linear interactions, cross-field comparisons, and domain-specific expressions.
The **expression computes a number**; the separate `operator` and bound parameters then compare that number against a threshold. The `parameters_json` keys are:
| Key | Required | Meaning |
|---|---|---|
| `expression` | yes | Math expression evaluated against the input data. Must produce a number. |
| `operator` | no (default `lte`) | One of `eq`, `lt`, `lte`, `gt`, `gte`, `between`. An unrecognized operator falls back to `lte`. |
| `threshold` | with the single-sided operators (default `0`) | The bound the computed value is compared against. |
| `min`, `max` | with `between` | Inclusive bounds: the constraint passes when `min <= computed <= max`. Either may be omitted for an open end. |
```json
{ "expression": "gwp_a1_a3 / total_mass", "operator": "lte", "threshold": 12.5 }
```
```json
{ "expression": "pe_content + pp_content", "operator": "between", "min": 0.30, "max": 0.78 }
```
Cross-field comparisons are written by **moving everything to one side** of the expression and comparing against `0`:
```json
{ "expression": "gwp_a1_a3 - base_gwp * (1 + 0.15 * recycled_fraction)", "operator": "lte", "threshold": 0 }
```
:::caution Common mistakes
- The bound key is **`threshold`**, not `value`. Unknown keys in `parameters_json` are silently ignored and `threshold` defaults to `0` — so `{"operator": "lte", "value": 1000}` actually enforces `<= 0`.
- Do **not** write the comparison inside the expression (`"a + b <= 1000"` with `operator: "eq"`). The engine evaluates the comparison to a boolean, coerces it to `1.0`/`0.0`, and compares *that* against the threshold — inverting your intent. Rearrange onto one side instead, as above.
- `eq` compares floats with a small tolerance (`1e-9`), not exact equality.
:::
#### Expression grammar
Expressions are evaluated with a sandboxed engine ([simpleeval](https://pypi.org/project/simpleeval/)) — no builtins, imports, or attribute access.
- **Variables**: every numeric input field is available by its `field_id` (numeric strings are coerced). Non-numeric fields are excluded — referencing one makes the constraint *not evaluable* rather than failing it. The special name `_value` holds the current value of the field the constraint is attached to. External inputs are referenced with their `ext__` prefix (declared per template — see [Reading the template](/guides/reference-epds-and-constraints#reading-the-template-to-understand-expected-data)).
- **Identifiers**: a field can only be referenced if its ID is a valid identifier — letters, digits, and underscores, not starting with a digit (`gwp_total` works; `gwp-total` cannot be referenced).
- **Functions**: `abs`, `min`, and `max` — `min`/`max` accept any number of arguments (`min(a, b, c)`). Nothing else works: no `round`, no `pow` (use `**`), and no `sum` — the sandbox rejects the list syntax `sum` needs, so use `a + b + c` instead.
- **Arithmetic**: `+`, `-`, `*`, `/`, `//`, `%`, `**` (exponent magnitude is capped for safety), unary `+`/`-`, and parentheses.
- **Result**: must be a number. Comparison and conditional syntax technically parse, but their boolean results coerce to `1.0`/`0.0` — keep comparisons in the `operator` field.
A division by zero or a reference to an absent field marks the constraint **not evaluable** (still reported as a violation); an unparseable expression marks it **misconfigured**. Neither silently passes.
:::caution Naming note
The term "parametric" is avoided here intentionally. In the EPD and LCA context it carries a specific meaning related to parameterized and variant EPDs. **Formula constraint** is used to prevent ambiguity in documentation and audit trails.
:::
### Allowed-values constraints
Checks a text (or multiselect) value against a fixed list of permitted strings — built for fields like LCI background datasets where the PCR publishes the exact list manufacturers may choose from.
The `parameters_json` keys are `values` (required, the list of allowed strings) and `case_sensitive` (optional, default `false` — comparison trims whitespace and ignores case). List inputs pass only when every item is allowed. Blank input passes: this constraint only polices *provided* values; required-ness is enforced separately by the field's `is_required`.
```json
{ "values": ["Ecoinvent 3.9", "Ecoinvent 3.10", "GaBi 2023"], "case_sensitive": false }
```
### Constraint model structure
Every constraint in the system follows the same data model:
| Field | Type | Description |
|---|---|---|
| `constraint_type` | string | One of: `range`, `statistical`, `formula`, `allowed_values` |
| `parameters_json` | object | Constraint-specific parameters — see each type above for its exact keys (`min`/`max`/`bounds_mode` for range; `distribution`/`threshold`/`mean`/`std_dev` for statistical; `expression`/`operator`/`threshold` or `min`/`max` for formula; `values`/`case_sensitive` for allowed_values). Unknown keys are silently ignored. |
| `severity` | string | `error` (blocks variant approve/auto-approve, and a reference EPD's submission when the template enforces its constraints unless the author [acknowledges it](/guides/reference-epds-and-constraints#submitting-a-reference-epd)), `warning` (surfaced to verifier), or `info` (informational). Default `error`. |
| `applies_if_json` | object or null | Optional conditional logic — when present, the constraint is only evaluated if the condition is true |
| `reference_field_id` | string | Template-level constraints only: the **anchor field** — see below |
| `label` | string | Template-level constraints only: display name shown in the editor and review surfaces |
#### The anchor field (`reference_field_id`)
Spec-level constraints are defined *on* an input field, so they anchor to it implicitly. Template-level constraints (entries of `template_constraints`) declare their anchor explicitly via `reference_field_id`. The anchor determines three things:
1. **Attribution** — violations report the anchor as their `field_id`, including for formula constraints that reference many fields in the expression.
2. **The required value** — when the constraint applies, the anchor field must have a value; a missing (or, for the numeric types, blank) anchor value is itself reported as a violation before the constraint logic runs. An unmet `applies_if_json` gate suppresses this too — a gated-off constraint reports nothing. (`allowed_values` is the exception for blanks: a *provided but blank* anchor passes, per its blank semantics above.)
3. **`_value`** — in formula expressions, `_value` is the anchor field's (numeric) value.
:::caution `reference_field_id` is required
Saving a template-level constraint with an empty `reference_field_id` is rejected with a `400 validation_error`. (Constraints stored before this validation existed are silently skipped during evaluation — they still appear in verifier checklists but never produce violations — so if you have a legacy unanchored constraint, re-save it with a real field ID.)
:::
### Conditional application
Any constraint type can be made conditional via `applies_if_json`. This handles product configurations where certain validation rules are relevant only for specific variants:
*Example: If the product contains recycled content, the recycled content fraction range constraint only applies.*
#### Shape
`applies_if_json` is a JSON object using a small grammar. A leaf condition names an input field, an operator, and an expected value:
```json
{
"field": "recycled_content",
"operator": "gt",
"value": 0
}
```
Supported operators:
| Operator | Meaning | Notes |
|---|---|---|
| `eq` *(default)* | Equal | String comparison |
| `neq` | Not equal | String comparison |
| `in` | Value is in a list | `value` must be an array |
| `gt`, `gte`, `lt`, `lte` | Numeric comparisons | Both sides coerced to `float` |
| `present` | Field has a non-null value | No `value` needed |
| `absent` | Field is missing or null | No `value` needed |
Leaf conditions can be combined with `all` (logical AND) or `any` (logical OR), and the combinators nest recursively:
```json
{
"all": [
{ "field": "category", "operator": "eq", "value": "concrete" },
{ "field": "strength_mpa", "operator": "gte", "value": 20 },
{
"any": [
{ "field": "admixture", "operator": "present" },
{ "field": "recycled_content", "operator": "gt", "value": 0 }
]
}
]
}
```
#### Semantics
- `null` or an empty object → the constraint always applies.
- Field references resolve against the same input data that drives constraint evaluation, so `applies_if_json` can gate a constraint on the value of any sibling field in the template.
- A numeric comparison (`gt`/`gte`/`lt`/`lte`) whose sides can't coerce to `float` falls through to "apply the constraint" — those operators fail open.
- The other operators evaluate against the missing value when the referenced field is absent: `eq`, `in`, and `present` then read as **unmet** (the constraint is skipped), while `neq` and `absent` read as met. Gate on fields you know will be present, or combine with a `present` condition.
- Send only the documented shape. Combinator values (`all`/`any`) must be arrays of condition objects — malformed shapes are outside the contract and can fail the validation call rather than being ignored.
:::caution Not the only `applies_if_json` in the system
Constraint-level `applies_if_json` is distinct from the requirement-level `applies_if_json` used for document search, and from `show_if_json` used for form-field visibility. Each has its own evaluator and its own shape — don't copy snippets between them.
:::
### Constraint type summary
| Constraint Type | Complexity | When to Use |
|---|---|---|
| **Range** | Low | First layer: catches data entry errors, enforces ratio bounds |
| **Statistical** | Medium | Outlier detection against population data (z-score) |
| **Formula** | Medium–High | Multi-variable or non-linear relationships not expressible as a simple range |
| **Allowed values** | Low | Text fields limited to a published list (e.g. permitted LCI datasets) |
---
## Sensitivity analysis
Before defining input range constraints, it is strongly recommended that the tool provider and verifier collaborate on a **sensitivity analysis** to identify which inputs have the greatest impact on EPD results.
A common approach is to identify inputs responsible for the **top 80% of GWP variance**. High-impact inputs should receive the tightest constraints, while lower-impact inputs can receive wider bounds.
```mermaid
graph TD
SA["Sensitivity Analysis Identify inputs driving top 80% of GWP variance"]
HIGH["High-Impact Inputs Tight, specific range constraints May use cross-dependencies"]
LOW["Lower-Impact Inputs Wider or simpler bounds Ratio-to-mass may suffice"]
TEMPLATE["Template Configuration Constraints calibrated to impact significance"]
SA --> HIGH
SA --> LOW
HIGH --> TEMPLATE
LOW --> TEMPLATE
style SA fill:#8B5CF6,color:#fff,stroke:#7C3AED
style HIGH fill:#EF4444,color:#fff,stroke:#DC2626
style LOW fill:#10B981,color:#fff,stroke:#059669
style TEMPLATE fill:#F59E0B,color:#fff,stroke:#D97706
```
This is a **recommended best practice**, not enforced by the platform. Sensitivity-driven constraint design leads to templates that are rigorous where it matters and flexible where it doesn't.
---
## Out-of-bounds handling
When a manufacturer attempts to generate a variant EPD with input values that fall outside the template's verified bounds, the platform enforces a structured response:
```mermaid
graph TD
INPUT(["Manufacturer enters input values"])
VALIDATE["Input Validation Check against template constraints"]
IN_BOUNDS{"All inputs within bounds?"}
PROCEED["Proceed to EPD generation"]
BLOCK["EPD Generation Blocked Out-of-bounds inputs identified"]
SHOW["Constraint Violations Displayed Which inputs, valid range, magnitude"]
CHOICE{"Manufacturer chooses path"}
ADJUST["Adjust Inputs Modify values to fall within bounds"]
REQUEST["Request Template Update Initiates Type 1 or Type 2 update"]
INPUT --> VALIDATE
VALIDATE --> IN_BOUNDS
IN_BOUNDS -->|"Yes"| PROCEED
IN_BOUNDS -->|"No"| BLOCK
BLOCK --> SHOW
SHOW --> CHOICE
CHOICE -->|"Adjust values"| ADJUST
ADJUST --> VALIDATE
CHOICE -->|"Request update"| REQUEST["Request Template Update"]
style INPUT fill:#6366F1,color:#fff,stroke:#4F46E5
style VALIDATE fill:#8B5CF6,color:#fff,stroke:#7C3AED
style IN_BOUNDS fill:#8B5CF622,stroke:#7C3AED,stroke-width:2px,color:#7C3AED
style PROCEED fill:#10B981,color:#fff,stroke:#059669
style BLOCK fill:#EF4444,color:#fff,stroke:#DC2626
style SHOW fill:#F59E0B,color:#fff,stroke:#D97706
style CHOICE fill:#F59E0B22,stroke:#D97706,stroke-width:2px,color:#D97706
style ADJUST fill:#06B6D4,color:#fff,stroke:#0891B2
style REQUEST fill:#EC4899,color:#fff,stroke:#DB2777
```
When an out-of-bounds input is detected:
- All violations are identified at once (not just the first one found)
- Each violation shows which input is out of range, what the valid range is, and by how much
- The "Generate EPD" action is disabled until all inputs are within bounds
- A "Request Template Update" action is available to initiate a template revision
---
## Related pages
- [Terminology](/concepts/terminology) -Constraint types reference and template versioning overview
- [Specifications & Validation](/concepts/specs-and-validation) -How the specification inheritance chain works
---
Source: https://docs.smartepd.com/concepts/roles-and-access.md
# Roles & Access Control
Smart EPD connects multiple organizations (manufacturers, tool providers, verifiers, and consultants) in shared workspaces. The permission model ensures every participant sees exactly what they need and can act only within their designated scope.
## Organization types
Every organization on Smart EPD has a type that determines its role:
```mermaid
graph TD
subgraph PLATFORM["Smart EPD Platform"]
MFG["Manufacturing Group Produces products, owns projects, creates and submits EPDs"]
TP["Tool Provider Develops LCA tools, defines specifications, supports tool verification"]
CON["Consultant Performs LCA work and EPD creation on behalf of manufacturers"]
VER["Verifier Reviews and certifies specifications, templates, and EPDs"]
end
style PLATFORM fill:#f8f9fb,stroke:#e5e7eb,stroke-width:2px,color:#374151
style MFG fill:#3B82F6,color:#fff,stroke:#2563EB
style TP fill:#10B981,color:#fff,stroke:#059669
style CON fill:#F59E0B,color:#fff,stroke:#D97706
style VER fill:#8B5CF6,color:#fff,stroke:#7C3AED
```
| Type | Primary responsibility | Typical workspace role |
|------|----------------------|----------------------|
| **Manufacturing Group** | Owns products and projects, submits EPDs for verification | Reporting Organization |
| **Tool Provider** | Registers tools in Categories, defines specifications, responds to verifier feedback | Tool Provider |
| **Consultant** | Performs LCA work and EPD authoring on behalf of manufacturers | Consultant |
| **Verifier** | Reviews specifications, templates, and EPDs for PCR compliance | Verifier |
## Permission layers
Access is determined at four levels, each scoping the next:
### 1. Organization membership
Every user belongs to exactly one organization with an internal role:
- **Admin** -Manages organization settings, members, and billing
- **Member** -Participates in workspaces and projects
### 2. Workspace membership
When an organization joins a workspace, all its members automatically receive **viewer** access. Individual users can be elevated:
- **Admin** -Manages workspace settings, Categories, organization memberships
- **Editor** -Creates and modifies projects, EPDs, submittals
- **Viewer** -Read-only access to all workspace content
### 3. Organization role in workspace
Each organization has exactly one role per workspace. These roles are not tied to permissions, but are used for documentation purposes. Typical responsibilities include:
| Role | What they typically do in the workspace |
|------|-----------------------------------------------------------------------------------------------------|
| **Reporting Organization** | Creates projects, authors EPDs, submits for verification |
| **Tool Provider** | Manages tool registration and specifications within Categories |
| **Consultant** | Authors EPDs and LCA documents on behalf of the reporting org |
| **Verifier** | Conducts section-level review, fills checklists, approves or flags items |
| **Member** | Default role for organizations without a specific designation; participates in workspace activities |
An organization can hold different roles in different workspaces. For example, a large firm might be a reporting organization in one workspace and a consultant in another.
### 4. Project team membership
Individual users are explicitly added to project teams. Team membership controls:
- **Can edit EPDs** -Whether the user can create and modify EPD content
- **Project visibility** -Only team members can view project content and participate in project chat
## How access cascades
```mermaid
graph TD
ORG["Organization User is a member"]
WS["Workspace Org joins → user gets viewer"]
CAT["Category Inherited from workspace"]
PROJ["Project Inherited from category + explicit invitiations"]
EPD["EPD Inhertied from project"]
ORG -->|"automatic"| WS
WS -->|"inherited"| CAT
CAT -->|"inherited + explicit"| PROJ
PROJ -->|"inherited"| EPD
style ORG fill:#3B82F6,color:#fff,stroke:#2563EB
style WS fill:#8B5CF6,color:#fff,stroke:#7C3AED
style CAT fill:#F59E0B,color:#fff,stroke:#D97706
style PROJ fill:#10B981,color:#fff,stroke:#059669
style EPD fill:#06B6D4,color:#fff,stroke:#0891B2
```
- **Organization → Workspace**: Automatic. All org members get viewer access when the org joins.
- **Workspace → Category**: Inherited. Workspace access extends to all Categories within it.
- **Category → Project**: Inherited + explicit. Workspace users get access. Other external users must be added to project teams individually.
- **Project → EPD**: Inherited from the project level
This layered model ensures that:
- A tool provider can see all Categories in a workspace but only edit the ones where their tool is registered
- A verifier can access all projects within their assigned Categories but cannot modify EPD content
- A manufacturer's external team members only see the specific projects they're assigned to
## Verifier-specific access
Verifiers have a unique access pattern. They are assigned to:
- **Categories** -For tool-level verification (verifying the tool and EPD Template)
- **Submittal packages** -For per-item review (verifying individual EPDs and LCAs)
Once assigned, verifiers see these items in their **verification queue**: a consolidated view across all active assignments.
Verifiers can:
- Read all EPD content for their assigned items
- Set conformance status per section
- Create verification comments
- View AI conformance hints
- Fill verifier checklists
- Approve, flag, or request revision on items
Verifiers **cannot**:
- Edit EPD content directly
- Create or delete EPDs
- Modify template constraints (they can approve or reject proposed constraints)
## API-level enforcement
Every v2 API endpoint is gated against a specific **scope**. Scopes are grouped into org-level and workspace-level sets, and each role (`admin`, `editor`, `viewer`) is granted a frozen scope set.
### Scope sets by role
| Scope group | `viewer` | `editor` | `admin` |
|---|---|---|---|
| **Org-level** | `organizations:read`, `plants:read` | + `plants:write` | + `organizations:admin` |
| **Workspace-level** | `workspaces:read` | + `workspaces:write` | + `workspaces:members`, `workspaces:invites` |
Roles are evaluated **per-resource**. A user can be `admin` in org A and `viewer` in org B; the scope check runs against the specific `org_id` or `workspace_id` in the request path. There is **no cascading** — org-admin does not grant workspace-admin, and vice versa.
### Capability matrix
| Capability | Viewer (org) | Editor (org) | Admin (org) | Viewer (ws) | Editor (ws) | Admin (ws) |
|---|:---:|:---:|:---:|:---:|:---:|:---:|
| Read plants | ✅ | ✅ | ✅ | — | — | — |
| Create / update plants | ❌ | ✅ | ✅ | — | — | — |
| Update org settings | ❌ | ❌ | ✅ | — | — | — |
| Create / revoke org invites | ❌ | ❌ | ✅ | — | — | — |
| Update / remove org members | ❌ | ❌ | ✅ | — | — | — |
| Create API keys | ❌ | ❌ | ✅ | — | — | — |
| Read workspace | — | — | — | ✅ | ✅ | ✅ |
| Update / delete workspace | — | — | — | ❌ | ✅ | ✅ |
| Create / remove workspace members | — | — | — | ❌ | ❌ | ✅ |
| Create / revoke workspace invites | — | — | — | ❌ | ❌ | ✅ |
| Create / update / delete projects, EPDs, PCFs | — | — | — | ❌ | ✅ | ✅ |
| Add / remove project team members | — | — | — | ❌ | ❌ | ✅ |
| Force-approve EPD (staff override) | — | — | staff/org-admin only | — | — | — |
### Workspace and PCC administration
`POST /api/v2/workspaces/` is a cross-resource endpoint — the user acts outside any existing workspace. Creating, updating, or deleting a workspace, and creating, updating, activating, or archiving a PCC (category) within one, all share the same authorization rule (SMA-2177):
- **Staff / superusers** can do all of these, in every environment, and (for workspace creation) must supply `primary_org_id` explicitly — they're creating on behalf of another org.
- **Everyone else** can do them only **outside production**, and only as an `admin` (`OrganizationMembership.role == 'admin'`) of an organization with the `can_create_workspaces` capability enabled. In production, a non-staff caller gets `403 forbidden` regardless of role — this is a hard platform restriction, not a permission you can be granted.
For workspace creation specifically, non-staff callers never choose the owning organization: the workspace is always owned by the caller's own qualifying org, resolved server-side, and any client-supplied `primary_org_id` is ignored.
**Practical effect for integrators**: in production, a non-staff API caller cannot create a workspace or a PCC at all. Use `GET /api/v2/workspaces/` and `GET /api/v2/workspaces/{id}/pccs/` to work with the ones SmartEPD has already provisioned for your organization — see the [API Quickstart](/guides/api-quickstart#2-find-your-workspace).
### Decorator patterns
The backend uses four permission decorators (see `smartepd/core/permissions.py`):
- `@require_scopes("organizations:admin", ...)` — resolves `org_id` from the URL path, checks the caller's `OrganizationMembership` role in that org.
- `@require_workspace_scopes("workspaces:members", ...)` — resolves `workspace_id` from the URL path, checks `UserWorkspaceMembership.user_role`.
- `@require_pcc_scopes(...)` / `@require_project_scopes(...)` — resolve PCC/project → workspace, with fallback to `OrgAccessGrant` and (for projects) `ProjectTeamMember`.
- `@require_jwt_auth` — blocks API-key authentication (used on auth/impersonation endpoints).
Endpoints that don't have a resource ID in the URL (e.g. `POST /api-keys/`, `POST /workspaces/`) use inline role checks via `is_org_admin()` or `is_org_admin_for()` selectors; this is documented in each endpoint's docstring.
### Workspace-scoped API keys
Every API key is bound to exactly one workspace. The auth layer attaches the key's `workspace_id` to the request, and:
- **Workspace-scoped permission decorators** (`@require_workspace_scopes`, `@require_project_scopes`, `@require_pcc_scopes`, `check_workspace_access`) reject requests targeting a different workspace by returning **404** (matching the existing cross-org IDOR convention).
- **List endpoints** that span workspaces (e.g. `GET /workspaces/`) filter to the key's single workspace, so an integrator never sees data outside the customer they're configured for.
- **Archived workspace** keys stop authenticating immediately — both because the workspace status check runs in the auth layer and because archiving a workspace explicitly revokes all its keys (`workspace_update_status`).
To create a workspace-scoped key, the caller must be:
1. An admin in some organization (`OrganizationMembership.role == 'admin'`), AND
2. An admin-level workspace member (`UserWorkspaceMembership.user_role == 'admin'`) on the target workspace.
Each organization may override the per-user key limit (default 3) via `Organization.api_key_limit`, so multi-customer integrators can hold one key per customer workspace.
### Frontend enforcement
The frontend mirrors these rules through a single capability-check layer:
- **`/api/v2/account/me`** returns `org_memberships` and `workspace_memberships` arrays — the full permission context in one request.
- **`usePermissions({ workspaceId })`** hook binds the current user's role/orgTypes to the `checkPermission` function in `src/v2/types/permissions.ts`.
- **``** component and **`useCan(capability, args)`** hook gate UI elements declaratively.
Frontend gating is a UX convenience — the backend scope check remains authoritative. A user who bypasses the UI will still receive `403` from the API.
## CRM access and contact data protection
The in-platform CRM is **staff-internal and workspace-agnostic**. It sits upstream of the platform — a deal becomes a project when it closes won — so none of the workspace-scoped rules above apply to it.
### Who can see the CRM
Access is the `is_staff` flag, enforced by `@require_staff`. That is a flat boolean with no further scoping: a staff account reaches every deal, whether or not it owns any of them. Two consequences worth stating plainly:
- **The staff flag is granted for unrelated reasons** — spec editing, EPD administration, developer tooling — and every holder gains read access to the commercial pipeline.
- **A feature flag cannot restrict this.** `require_feature_flag` unconditionally bypasses for staff and superusers, so a flag placed over a staff-only surface is inert and can never act as a kill switch.
Writes to a deal's **commercial figures** are narrower than reads. `amount`, `amount_base` and `exchange_rate` may only be changed by the deal's owner or a superuser, enforced in `crm.services.deal_assert_money_fields_writable`. Reads stay open to all staff so board aggregates keep working. Deals with no owner — expected during the Zoho import, where some Zoho owners map to no platform account — stay writable by any staff user, so the records ops most needs to repair are not locked away.
### Contact records and the right to erasure
`crm.Contact` describes **people who never signed up for the platform**. They are imported in bulk from Zoho, and some are EU residents. This makes them categorically different from `User` records, which belong to people who accepted terms.
A GDPR Article 17 ("right to be forgotten") request therefore cannot be satisfied by hiding the row, because the same personal data is retained in four further places:
| Where | Why it survives a soft delete |
|---|---|
| `crm_contactevent` | `Contact` is tracked `append_only`, which compiles to a real Postgres trigger blocking updates and deletes. Every historical value of the name, email and phone is kept there. |
| `crm_notes` + `crm_crmnoteevent` | A note can be written about a contact and its body is unrestricted free text, so names and numbers land there routinely — and its history is append-only too. |
| `zoho_onboarding_emails` | The Zoho mirror stores the `recipient` address plus `from`/`to` inside its `raw` payload, independently of the contact record. |
| `accounts.User` | If the contact shadows a platform user, the link alone re-identifies the tombstone. |
`crm.services.contact_erase` reaches all of them. It blanks every personal column in place, keeps the row as a tombstone so deal participation history stays readable, severs the link to any platform user (the user's own account is untouched — they have their own lawful basis), blanks the bodies of notes written about them, deletes both sets of audit rows through an explicit and narrowly-scoped suspension of the append-only triggers, and strips the address out of the Zoho mirror. It then writes a `ContactErasureLog` row — which, because the audit trail itself has just been destroyed, is the only durable evidence that the request was honoured.
**Erasure has to survive the sync, not just happen once.** Purging the mirror is a point-in-time rewrite, and the Zoho email sync runs on a six-hourly beat that re-imports the same records — so on its own the address reappears within hours, and because the contact is already flagged erased, re-running the erasure short-circuits and can never clean it again. The erasure log therefore stores a **keyed HMAC of the address**, and the mirror's upsert path consults it on every write: a suppressed address is dropped at the point it would enter Postgres. It is keyed on `SECRET_KEY` rather than a plain hash because the space of email addresses is small enough to enumerate, and an unkeyed digest would be reversible — which would put the personal data back into the very row that exists to record its destruction.
Erasure is exposed **only** as a management command, and dry-run is its default:
```bash
python manage.py crm_contact_erase --contact-id --actor-email --reason ""
```
It is not an API endpoint. The operation is rare, irreversible and legally triggered, so it needs no UI, and requiring production shell access is a tighter gate than any decorator. Add `--confirm` to execute. There is no undo.
## Related pages
- [Platform Overview](/getting-started/platform-overview) -The entity hierarchy that permissions follow
- [Data Model](/concepts/data-model) -Technical entity reference including membership models
- [Verification Workflow](/workflows/verification-workflow) -How verifier access is used in practice
---
Source: https://docs.smartepd.com/workflows/onboarding-workflow.md
# Onboarding & Setup
This guide walks through how organizations, workspaces, and projects are set up before EPD work begins. Every step described here is available both through the Smart EPD UI and the API.
## Step 1: Accept your invite and set up your organization
An **Organization** is your company's identity on Smart EPD. Onboarding begins when you receive an email invite to join the platform — either from Smart EPD directly or from an existing partner organization that's bringing you in.
1. **Accept the invite** — click the link in the email to create your user account.
2. **Enter your organization information** — company name, type (see the table below), logo, website, and contact details. The organization record is created at this point, and you become its first admin automatically.
3. **Invite your fellow users** — send email invites to anyone else at your company who needs access. New users join your organization as `user`-role members by default; you can elevate them to `admin` later.
## Step 2: Invite partner organizations
Smart EPD supports two ways to bring partners onto the platform:
- **Invite an existing organization** -If your partner already has a Smart EPD account, link your organizations directly.
- **Create and invite** -If your partner is new to Smart EPD, you can create their organization and send an invite in one step. The invitee receives an email with a link to accept and complete setup.
Partner relationships are bidirectional. Once established, both organizations can collaborate in workspaces.
## Step 3: Create a Workspace
A **Workspace** is a shared collaboration space where two or more organizations work together. Think of it as a project room: it defines *who* is at the table and *what role* each organization plays.
```mermaid
graph LR
subgraph WS["Workspace: Acme 2025 EPDs"]
direction TB
ORG_M["Acme Concrete Reporting Organization"]
ORG_C["LCA Consultants Consultant"]
ORG_V["Green Verify Verifier"]
end
style WS fill:#7C3AED22,stroke:#7C3AED,stroke-width:2px,color:#7C3AED
style ORG_M fill:#3B82F6,color:#fff,stroke:#2563EB
style ORG_C fill:#10B981,color:#fff,stroke:#059669
style ORG_V fill:#8B5CF6,color:#fff,stroke:#7C3AED
```
When you create a workspace:
1. **Name it** -typically after the engagement (e.g., "Acme 2025 EPDs")
2. **Add organizations** -each with a role: `reporting_organization`, `tool_provider`, `consultant`, or `verifier`
3. **Members cascade** -all members of an invited organization automatically receive viewer access to the workspace
You can add more organizations or invite individual users at any time.
## Step 4: Create a Category
A **Category** defines how a product category's EPDs are created within a workspace. It's the governance layer, setting the rules before any work begins.
When creating a Category, you configure:
| Setting | Description |
|---------|-----------------------------------------------------|
| **Name** | The product category (e.g., "Ready-Mixed Concrete") |
| **Mode** | How EPDs will be produced (see below) |
| **PCR Specification** | Which Product Category Rules apply |
| **Tool** | Which LCA tool/model is used |
#### How the tool selection shapes the EPD flow
The specific tool you select governs which EPD flows are available within the Category. Each registered tool carries an **`allows_reference_epds`** flag that the Category inherits at creation time:
- **Tools with `allows_reference_epds` enabled** — projects under this Category support the full **reference + variant** flow. A manufacturer can author a standard draft, morph it into a reference EPD (which locks a modeling envelope via an EPD Template), and then generate variant EPDs that inherit the reference's template and constraints.
- **Tools with `allows_reference_epds` disabled** — projects under this Category support only standalone **standard EPDs**. The "Make Reference EPD" action is hidden in the UI and the corresponding API endpoint returns a validation error.
In other words, the tool itself decides whether a Category is a free-form path or a templated reference/variant path. Choose the tool deliberately: switching later means re-authoring any EPDs that depend on the locked envelope.
Once configured, **activate the Category** to start creating projects within it.
:::note Tool vs Platform
The **tool** (or model) is the specific LCA configuration selected at the Category level. The underlying **platform** (e.g., SimaPro, GaBi) goes through a separate platform pre-verification process before it becomes available for selection. See [Terminology](/concepts/terminology) for details.
:::
## Step 5: Create a Project
A **Project** represents a single product being assessed. It inherits its PCR, tool configuration, and designated verifier organization from the parent Category — you do *not* assign a verifier at the project level.
When creating a project:
1. **Name it** after the product (e.g., "EcoCrete Mix 7")
2. **Add external team members** — invite individual users who aren't already covered by an existing workspace membership. Workspace members already get project access through that membership, so you only need to add users from outside the workspace member organizations (e.g., a consultant brought in for this project specifically, or an external reviewer). Each external member can be granted per-project permissions such as `can_edit_epds`.
Projects can be duplicated within the same Category for similar products.
## Step 6: Upload LCA documentation
Before EPDs can be created and verified, the project's LCA documentation should be prepared. The LCA submission is closely tied to tool/model verification — the verifier reviews the LCA to confirm that the tool produces valid results within the project's scope.
- **Upload documents** — LCA methodology report, background data documentation, supporting files
- **Submit for review** — Makes the LCA available for the verifier to review
The LCA must be approved before any EPDs in the project can be approved (see [approval gates](/workflows/epd-lifecycle#approval-gates)).
## Step 7: Create EPDs
With the project configured and LCA documentation prepared, create EPDs. For tool-based Categories, EPD creation involves resolving an **EPD Template** from the specification chain — the template pre-fills locked fields, defines validation constraints for unlocked fields, and is submitted for verifier review as part of the overall verification process.
| EPD Type | Description | When to use |
|----------|-------------|-------------|
| **Standard** | Standalone EPD | Manual Categories, individual assessments |
| **Reference** | Linked to an EPD Template with locked fields and constraints | Tool-based Categories, verification artifacts |
| **Variant** | Derived from an approved reference EPD with different parameters | Production EPDs in tool-based Categories |
## Step 8: Submit for verification
Bundle the LCA documentation and EPDs into a **submittal package** for third-party verification. The verifier reviews all items as a unit, with dependency gates ensuring the LCA is approved before EPDs.
At this point, the project is fully set up and ready for the [EPD Lifecycle](/workflows/epd-lifecycle) workflow.
:::info API Access
Every step in this workflow is available via the Smart EPD API. Tool providers can fully automate onboarding, enabling self-service setup for manufacturers directly from within their own tools.
:::
## Putting it all together
```
Organization (Acme Concrete)
├── Partner: Green Verify Inc (verifier)
├── Partner: LCA Consultants Ltd (consultant)
│
└── Workspace: "Acme 2025 EPDs"
├── Members: Acme (reporting_org), Green Verify (verifier)
│
└── Category: "Ready-Mixed Concrete"
├── Specification: EN 15804+A2
├── Tool: SimaPro (pre-approved)
├── Verifier: Green Verify Inc
│
└── Project: "EcoCrete Mix 7"
├── Team: workspace members (auto) + external consultant (invited)
├── Verifier: inherited from Category (Green Verify Inc)
├── LCA Version 1 (uploaded, ready for review)
├── EPD Template (resolved from specification chain)
├── Reference EPD (linked to template)
├── Standard EPD #1
└── Submittal Package (bundles LCA + EPDs for verification)
```
## What's next
- [EPD Lifecycle](/workflows/epd-lifecycle) -How EPDs move from draft through verification to publication
- [Verification Workflow](/workflows/verification-workflow) -How verifiers review and approve EPDs
---
Source: https://docs.smartepd.com/guides/api-quickstart.md
# API Quickstart
Walk through the full Smart EPD workflow using the API: from creating a workspace to publishing automated variant EPDs. Every step includes a `curl` example you can run against the sandbox environment.
:::tip Prerequisites
- A Smart EPD user account that can sign in to the web UI
- That user must be an **organization admin** (needed to generate API keys and to use the non-prod force shortcuts in steps 11 and 13)
- `curl` and `jq` installed
:::
## 1. Generate an API key
Sign in to the Smart EPD web UI, open your account settings, and generate a new API key. Copy the key the moment it's shown — it's only displayed once, and you won't be able to retrieve it again later.
Export it as `TOKEN` so every subsequent `curl` example works unchanged:
```bash
export TOKEN="sk_your_api_key_here"
```
All subsequent requests use `Authorization: Bearer $TOKEN`. API keys are the recommended auth for scripting and CI/CD — they don't expire on the short cadence JWTs do. For the non-production force-approve shortcuts later in this guide, the key must belong to an admin.
:::note JWT alternative
If you'd rather authenticate interactively with an email-code login and a short-lived JWT instead of generating an API key, see the [Authentication](/getting-started/authentication) page for the `/api/v2/auth/email-code/` endpoints.
:::
## 2. Find your workspace
An API key is **scoped to a single workspace** — the one it was generated in (step 1). Calls that target any other workspace return `404`, so the rest of this guide operates inside the key's own workspace. List your workspaces to get its id; with an API key this returns exactly that one bound workspace:
```bash
curl -s https://sandbox.smartepd.xyz/api/v2/workspaces/ \
-H "Authorization: Bearer $TOKEN" | jq -r '.data[0].id'
```
Save it as `$WS_ID`.
:::note Creating workspaces
`POST /api/v2/workspaces/` exists, but **in production it's staff-only** — a non-staff caller gets `403 forbidden` no matter their role. Outside production (staging, sandbox, your local dev stack), a qualifying org-admin can use it. See [Workspace and PCC administration](/concepts/roles-and-access#workspace-and-pcc-administration) for the full rule.
Even where it's available, a workspace-scoped API key can't operate inside a workspace it just created, so the quickstart uses the workspace your key already belongs to rather than minting a new one. **Against the production API, don't try to create a workspace at all** — list the ones already provisioned for your organization instead.
:::
## 2a. Register a manufacturing plant
Plants are **organization-scoped** — registered once and reused across every EPD your organization authors. First resolve the organization your API key belongs to:
```bash
curl -s https://sandbox.smartepd.xyz/api/v2/organizations/mine/ \
-H "Authorization: Bearer $TOKEN" | jq -r '.[0].id'
```
Save the returned id as `$ORG_ID`, then create a plant under it:
```bash
curl -X POST https://sandbox.smartepd.xyz/api/v2/organizations/$ORG_ID/plants/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Atlantic Batch Plant #1",
"address": "123 Industrial Pkwy",
"pluscode": "87JC9W8Q+2V"
}'
```
Save the `id` as `$PLANT_ID`. The same `/api/v2/organizations/$ORG_ID/plants/` path supports listing (`GET`), fetching a single plant (`GET .../$PLANT_ID/`), updating (`PATCH`), and archiving (`POST .../$PLANT_ID/archive/`). Plant details also surface in the EPD document's `plants` block (see step 6).
## 3. Create a category (PCC)
A category ties your projects to a specific PCR spec and a pre-approved LCA tool. The PCC's mode (manual vs. platform) is now derived from the selected tool — you no longer pass `mode` in the request body.
:::caution Production restriction
Category creation follows the same rule as workspace creation: **staff-only in production**. A non-staff caller against the production API gets `403 forbidden` on the call below. If you're integrating against production, list the categories already set up in your workspace instead —
```bash
curl https://app.smartepd.com/api/v2/workspaces/$WS_ID/pccs/ \
-H "Authorization: Bearer $TOKEN" | jq '.data[] | {id, name}'
```
— and use the `id` of the one that matches your PCR/tool as `$PCC_ID` for the rest of this guide, skipping straight to [step 4](#4-create-a-project).
:::
```bash
curl -X POST https://sandbox.smartepd.xyz/api/v2/workspaces/$WS_ID/pccs/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Concrete Products",
"pcr_spec_id": "",
"preapproved_tool_id": ""
}'
```
:::info Finding the spec ID
List available specs with `GET /api/v2/specs/`. The endpoint only returns PCR specs that have a published revision — exactly the set of specs a PCC can link to. The `id` field is your `pcr_spec_id`.
:::
:::info Finding a tool that supports reference EPDs
List pre-approved tools with `GET /api/v2/tools/preapproved/`. If you plan to run the full variant-EPD flow (step 8 onwards), pick a tool whose response has `allows_reference_epds: true` — otherwise `morph-to-reference` will reject the EPD.
:::
Save the `id` as `$PCC_ID`.
Then activate the PCC so its projects accept EPDs. Depending on the selected spec, the PCC may already be active after creation — in that case the activate call returns a `400` with a message explaining it's already active, which is safe to ignore.
```bash
curl -X POST https://sandbox.smartepd.xyz/api/v2/pccs/$PCC_ID/activate/ \
-H "Authorization: Bearer $TOKEN"
```
## 4. Create a project
```bash
curl -X POST https://sandbox.smartepd.xyz/api/v2/pccs/$PCC_ID/projects/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Ready-Mix Concrete A"}'
```
Save the `id` as `$PROJECT_ID`.
## 5. Create an EPD
LCA author metadata (`lca_author_name`, `lca_author_email`, `lca_author_company`) can be set on the create call so you don't need a follow-up `PATCH` just for attribution:
```bash
curl -X POST https://sandbox.smartepd.xyz/api/v2/projects/$PROJECT_ID/epds/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"lca_author_name": "Sofia Martinez",
"lca_author_email": "sofia@lca-consultants.example.com",
"lca_author_company": "LCA Consultants Inc."
}'
```
Save the `id` as `$EPD_ID`. The EPD starts in `draft` status.
## 6. Discover the EPD schema
Before filling in data, check what fields the EPD expects:
```bash
curl https://sandbox.smartepd.xyz/api/v2/epds/$EPD_ID/schema/ \
-H "Authorization: Bearer $TOKEN" | jq '.input_field_defs[] | {field_id, type, is_required, label}'
```
This returns the resolved input field definitions (with types, required flags, options, and constraints) and the requirements that will be checked against the EPD. Field IDs use hyphenated names (e.g., `product-name`, `allocation-procedure`).
For a slim, integrator-friendly response (renamed keys, no inheritance metadata or legacy duals) append `?view=summary`:
```bash
curl "https://sandbox.smartepd.xyz/api/v2/epds/$EPD_ID/schema/?view=summary" \
-H "Authorization: Bearer $TOKEN" | jq '.fields[] | {id, type, required, label}'
```
See [EPD Schema](/concepts/epd-schema) for the full field-by-field decode of both views, including constraint shapes and conditional logic.
## 7. Fill EPD document and sections
Store the full EPD document (product data, impacts, manufacturer info). All required fields from the schema endpoint (step 6) must be populated or the EPD will fail at submission with a `shape_validation_error`.
```bash
curl -X PUT https://sandbox.smartepd.xyz/api/v2/epds/$EPD_ID/document/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"product_name": "Ready-Mix Concrete 4000 psi",
"document": {
"product-name": "Ready-Mix Concrete 4000 psi",
"product-description": "Ready-mix concrete with nominal compressive strength of 4000 psi.",
"product-specificity": "product_specific",
"product-image": "https://placehold.co/400x300?text=Concrete",
"applicable-in": ["United States", "Canada"],
"declared-unit": "1 m3",
"functional-unit": "1 m3",
"unit-type": "declared",
"mass-per-unit": "2400 kg",
"manufacturer-name": "Atlantic Building Products",
"manufacturer-address": "100 Industrial Ave",
"manufacturer-city": "Boston, MA 02118",
"manufacturer-country": "United States",
"manufacturer-phone": "+1 (617) 555-0100",
"manufacturer-website": "https://atlanticbuildingproducts.example.com",
"contact-email": "epd@atlanticbuildingproducts.example.com",
"organization-logo": "https://placehold.co/200x60?text=Logo",
"general-organization-info": "Leading manufacturer of ready-mix concrete since 1985.",
"scope": "Cradle to gate",
"lca-software": "SimaPro",
"lca-software-version": "9.5",
"primary-data-year": "2024",
"allocation-procedure": "
Mass-based allocation for co-products.
",
"cut-off-procedure": "
Flows below 1% of total mass excluded per EN 15804+A2.
",
"results-data": {
"gwp-total@ipcc-ar6": {"a1": 252.0, "a2": 35.8, "a3": 35.3},
"wdp@ef-3.1": {"a1": 4.2, "a2": 0.3, "a3": 1.1}
}
}
],
"used-renewable-electricity": true,
"electricity-source": "Offsite",
"renewable-type": "Wind + Solar PPA",
"commitment-pledged": "Yes",
"accounting-methodology": "Market-based"
},
"display_config": {"impacts": true, "resource_uses": true, "output_flows": true, "additional_results": true}
}'
```
:::info The fifth results table is shaped differently
`impacts`, `resource_uses` and `output_flows` above use the legacy flat-row
form, where each row carries its own `unit`. **`additional_results` does not.**
It is the optional, manufacturer-defined table and it runs in *catalog mode*:
rows live in a `results-data` matrix, the row key is an id from the LCIA
indicator catalog (`gwp-total@ipcc-ar6`), and label, unit and methodology are
resolved from the catalog when the EPD renders. Sending a `unit` for a catalog
row is rejected. Fetch the legal ids from
`GET /api/v2/specs/indicator-catalog/`, and see
[Writing Results Data](/guides/writing-results-data#the-additional-indicators-table-catalog-mode)
for the full contract. Omit the key entirely if you have nothing extra to report.
:::
:::caution Document field IDs
Most document keys are the **hyphenated field IDs** from the schema endpoint (e.g. `product-name`, `allocation-procedure`). A few fields are *bound* to a different key, and the results tables are the ones you will hit: the field id is `resource-uses` but the payload above writes `resource_uses`. Either spelling is accepted on write, and validation reads both. The document is stored **verbatim**, so `GET /epds/{id}/` returns exactly the keys you sent — a table written as `resource-uses` is not missing, it is simply still called that. Only `GET /epds/{id}/view/` adds the bound-key alias. Pick one spelling and stay with it; the bound key is the one both endpoints agree on. See [Writing Results Data](/guides/writing-results-data#where-results-live). Select fields use string values (e.g., `"1 m3"`), and multiselect fields use arrays of strings (e.g., `["a1", "a2", "a3"]`). Rich text fields accept HTML (e.g., `"
Content here.
"`).
:::
:::info Why set `used-renewable-electricity: true`
Four fields in the renewable-electricity block (`electricity-source`, `renewable-type`, `commitment-pledged`, `accounting-methodology`) are gated by a `show_if_json` rule — they only become required when `used-renewable-electricity` is `true`. Setting it true here means the reference EPD exercises the full renewable-electricity branch. Variants created from this reference inherit those 4 values along with the rest of its document; a variant only sends them in `field_inputs` if its own values differ. Set it to `false` (or omit it) if you want a shorter required-field set.
:::
## 8. Convert to a Reference EPD
If you want to create variant EPDs later, convert the standard EPD to a reference EPD:
```bash
curl -X POST https://sandbox.smartepd.xyz/api/v2/epds/$EPD_ID/morph-to-reference/ \
-H "Authorization: Bearer $TOKEN"
```
The EPD's `epd_type` changes from `standard` to `reference`. This auto-creates a resolved template from the project's spec. See [Reference EPDs & Constraints](/guides/reference-epds-and-constraints) for details.
### Lock methodology fields on the reference
Now that the EPD is a reference, apply the authoring policy that every variant derived from it will inherit. Locks belong on reference EPDs specifically — setting them earlier on the standard EPD wouldn't be meaningful because variants only exist off reference EPDs.
Re-PUT the document with the `authoring_policy.locked_fields` list. `PUT /document/` requires the full `document` body (it's a replacement, not a patch), so re-send the same `document` from step 7 and add the `authoring_policy` field. `display_config` is optional — omitting it leaves the value from step 7 unchanged:
```bash
curl -X PUT https://sandbox.smartepd.xyz/api/v2/epds/$EPD_ID/document/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"product_name": "Ready-Mix Concrete 4000 psi",
"document": { /* same body as step 7 */ },
"authoring_policy": {
"locked_fields": [
"lca-software",
"lca-software-version",
"primary-data-year",
"declared-unit",
"references"
]
}
}'
```
:::info Locking fields for variants
`authoring_policy.locked_fields` is an optional list of field IDs that variant EPDs derived from this reference cannot modify. The typical use case is locking **methodology** fields (LCA software, data year, unit of measure, reference standards) so every variant inherits the same methodological baseline — variants differ only in product-specific values like impacts and plant data. The server rejects any later variant document update that diverges from the reference on a locked field with a `400 validation_error`.
This is distinct from the **template-level** `locked_fields` exposed by the resolved template (see step 9) — those come from the PCR spec's field definitions and are fixed for every EPD using that spec, while `authoring_policy.locked_fields` is **per-reference** and is set by the reference author here.
:::
## 9. Configure template constraints (optional)
Constraints live on the **reference EPD's template** — morphing to reference in step 8 auto-created one. Add constraints and external input fields now so variant EPDs generated from this reference are validated against them.
```bash
curl -X PUT https://sandbox.smartepd.xyz/api/v2/epds/$EPD_ID/template/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"template_constraints": [
{
"constraint_type": "range",
"parameters_json": {"min": 0, "max": 1000},
"severity": "warning",
"reference_field_id": "gwp-total",
"label": "GWP total within reasonable bounds"
}
],
"external_input_fields": [
{"field_id": "ext__strength_class", "label": "Strength Class", "data_type": "string", "required": true},
{"field_id": "ext__cement_content", "label": "Cement Content (kg/m3)", "data_type": "number", "required": true}
],
"constraints_enforced": true
}'
```
Try the constraints on sample data before going further. This is a what-if: nothing is saved, and the reference EPD is not checked against these values.
```bash
curl -X POST https://sandbox.smartepd.xyz/api/v2/epds/$EPD_ID/template/test-constraints/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"epd_field_values": {"gwp-total": 320.5},
"external_payload": {"strength_class": "C30/37", "cement_content": 320}
}'
```
Then enter **this reference EPD's own values** for the external input fields. Until it is first submitted, the template is shared with the project's other draft reference EPDs, but each reference EPD supplies its own values, and these are what submission (step 12) checks. Keys may omit the `ext__` prefix, and the body replaces any values already saved:
```bash
curl -X PUT https://sandbox.smartepd.xyz/api/v2/epds/$EPD_ID/reference-inputs/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"reference_inputs": {"strength_class": "C30/37", "cement_content": 320}}'
```
To see how the saved values fare against every constraint, and whether submission would accept them (`would_block`), call `POST /api/v2/epds/$EPD_ID/reference-inputs/evaluate/`. See [Entering the reference values](/guides/reference-epds-and-constraints#entering-the-reference-values) for the validation rules.
:::tip
Only EPDs of type `reference` have a template — standard EPDs return 400 on these endpoints. Make sure step 8 (morph-to-reference) ran successfully before configuring constraints.
:::
## 10. Upload LCA documents
Create an LCA version and attach supporting documents:
```bash
# The LCA version is auto-created when you first access it
curl https://sandbox.smartepd.xyz/api/v2/projects/$PROJECT_ID/lca/ \
-H "Authorization: Bearer $TOKEN"
# Upload a document (multipart form)
curl -X POST https://sandbox.smartepd.xyz/api/v2/projects/$PROJECT_ID/lca/documents/ \
-H "Authorization: Bearer $TOKEN" \
-F "file=@/path/to/lca-report.pdf" \
-F "document_type=lca_report" \
-F "stage=lca_submittal"
```
## 11. Submit LCA for verification
In non-production environments, pass `force_status: "approved"` in the request body to skip the verifier workflow entirely and land the LCA in `approved` state in one call. Omit the body (or send an empty object) for the normal flow, which transitions the LCA to `in_review` and waits on a verifier.
```bash
curl -X POST https://sandbox.smartepd.xyz/api/v2/projects/$PROJECT_ID/lca/submit/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"force_status": "approved"}'
```
See [Verification Testing Shortcuts](/guides/verification-testing) for all force-approve options. Force shortcuts are rejected with a 400 in production.
## 12. Submit EPD for verification
```bash
curl -X POST https://sandbox.smartepd.xyz/api/v2/epds/$EPD_ID/submit/ \
-H "Authorization: Bearer $TOKEN"
```
This validates the document against the spec's required fields, runs constraint checks, and creates a verifier checklist. If required fields are missing, submission is blocked with a `shape_validation_error`.
For reference EPDs, the template's constraints are checked against the EPD's document together with its own reference values from step 9. The template's `sample_external_payload` is not used. If `constraints_enforced` is `true` on the template:
- a missing value for a `required` external field, or for one a constraint needs, blocks submission with `400 reference_inputs_missing`, whose `details` name the fields;
- a failing error-severity constraint blocks with `400 constraint_failures`, whose `details` list the failing constraints. To submit anyway and leave the decision to the verifier, repeat the call as `POST .../submit/?acknowledge_constraint_failures=true`. The acknowledgement is recorded on the EPD and shown to the verifier;
- warning and info failures never block.
With `constraints_enforced: false`, nothing blocks. Either way, the verifier reviews each constraint on the submitted values. See [Submitting a reference EPD](/guides/reference-epds-and-constraints#submitting-a-reference-epd).
Submitting a reference EPD also freezes its constraints: the template becomes the EPD's own `v1`, and `PUT .../template/` answers `409 template_locked` until the verifier requests changes. After approval, changing them takes a revision (`POST /api/v2/epds/$EPD_ID/revision/`). See [Constraint versions and the edit lock](/guides/reference-epds-and-constraints#constraint-versions-and-the-edit-lock).
Submission also records the EPD's **engine snapshot**: the EPD tool and version, PCR spec version and constraint version it was submitted against. Approval locks the snapshot, and variants are compared with it. See [Submission snapshots and the model lock](/guides/reference-epds-and-constraints#submission-snapshots-and-the-model-lock).
:::caution
`force_status` on verification approval does **not** bypass document validation. The EPD must be successfully submitted first — force-approve only skips the checklist sign-off and reviewer workflow.
:::
## 13. Approve EPD verification (non-prod shortcut)
In non-production environments, the `/force-approve/` endpoint collapses the full verification flow (assign verifier → start review → approve) into a single call. It creates or reuses an assignment internally, so you don't need to fetch a user ID or track an assignment ID.
```bash
curl -X POST https://sandbox.smartepd.xyz/api/v2/epds/$EPD_ID/force-approve/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"force_status": "approved"}'
```
:::caution Non-prod only
Force-approve is rejected in production with a 400. The caller must be an **organization admin** — regular workspace members get a 403. For the Quickstart flow, make sure the API key you generated in step 1 belongs to an org-admin user.
:::
This skips the checklist sign-off gate, including the reference EPD's constraint review, and approves the underlying EPD in one step. Approval still locks the reference EPD's model, as it does on the verifier's path.
## 14. Create variant EPDs
With an approved reference EPD and approved LCA, you can now generate variants. The payload has two separate sections:
- **`parameters`** — external input values (constraint inputs) whose keys match the external input fields from step 9, without the `ext__` prefix.
- **`field_inputs`** — overrides for **unlocked** EPD document fields, keyed by field id (e.g. `product-name`; the bound document key such as `product_name` is accepted too). A variant **inherits the reference EPD's whole document**, so send only what this variant changes — or nothing at all.
Two distinct lock sources apply, and a variant can change neither:
- **Template-level locks** — the `locked_fields` list from the resolved template. Fixed by the PCR spec itself and shared across every EPD using the spec.
- **Reference-level locks** — the `authoring_policy.locked_fields` list set by the reference author in step 7. Per-reference.
An override naming a locked field, or no field of the EPD, is refused with a `400 validation_error` that lists the keys — at creation and on every update. (External parameters — `ext__…` — belong in `parameters`, not `field_inputs`.)
```bash
curl -X POST https://sandbox.smartepd.xyz/api/v2/epds/$EPD_ID/variants/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"parameters": {"strength_class": "C30/37", "cement_content": 320},
"field_inputs": {
"product-name": "Ready-Mix Concrete 4000 psi (C30/37 variant)"
}
}'
```
The variant EPD is created in `generated` status, carrying the reference document with your overrides applied (and the reference's product name unless you override it).
To change what a variant overrides later, while its EPD is a draft or has changes requested, either:
- `PUT /api/v2/epds/$EPD_ID/variants/$VARIANT_ID/field-inputs/` with `{"field_inputs": {...}}` — replaces the set of overrides; each value is written into the variant's document; or
- `PUT /api/v2/epds/{variant_epd_id}/document/` — edit the variant's document directly, as the SmartEPD editor does. Changing a locked field is refused either way.
:::info Submitting a variant
Submit with `POST /api/v2/epds/{variant_epd_id}/submit/`, using the variant's own `epd_id` from the create response. The variant's document is validated like any EPD's (required fields, types) — inherited values count, so a variant that changes nothing submits as it is. The quickstart's happy path goes straight to auto-approve (step 15).
:::
## 15. Auto-approve variants
Variants that pass automated constraint checks can be auto-approved:
```bash
curl -X POST https://sandbox.smartepd.xyz/api/v2/epds/$EPD_ID/variants/$VARIANT_ID/auto-approve/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
If constraints block the auto-approve, you can use the "force" shortcut (testing only, not available in production):
```bash
curl -X POST https://sandbox.smartepd.xyz/api/v2/epds/$EPD_ID/variants/$VARIANT_ID/auto-approve/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"force_status": "approved"}'
```
## Next steps
- [Reference EPDs & Constraints](/guides/reference-epds-and-constraints) — understand how constraints work
- [LCA Submission](/guides/lca-submission) — detailed LCA workflow
- [Variant EPDs](/guides/variant-epds) — deep dive into variant generation and automated verification
- [Verification Testing Shortcuts](/guides/verification-testing) — all `force` options
- [Cover Page Preview](/guides/cover-page-preview) — render an EPD's cover page on your own platform
- [API Reference](/api-reference) — interactive endpoint explorer
---
Source: https://docs.smartepd.com/guides/agent-integration.md
# AI Agent Integration
This guide is for teams connecting an **AI agent** to Smart EPD — a coding agent (Claude Code, Cursor, Copilot Workspace), an internal assistant, or a custom LLM application that drafts and validates EPDs programmatically.
The Smart EPD v2 API is a good fit for agents: every EPD exposes its own field schema, validation is a separate read-only call you can loop on before submitting, and errors use one stable machine-readable envelope. This page tells you how to load the right context into your agent and which workflow to point it at.
:::tip TL;DR
1. Give your agent the [copy-paste context block](#ground-your-agent-copy-paste-context) below.
2. Point it at the sandbox (`https://sandbox.smartepd.xyz`) with an `sk_...` API key.
3. Have it follow the [draft → validate → submit loop](#the-draft--validate--submit-loop).
:::
## Machine-readable doc surfaces
Everything on this docs site is published in agent-friendly formats. Pick the surface that matches how much context your agent can hold:
| Surface | URL | Use it when |
|---|---|---|
| **Docs index** | [`/llms.txt`](https://docs.smartepd.com/llms.txt) | The front door. A curated map of every page with one-line descriptions, in [llms.txt](https://llmstxt.org/) format. Fetch this first and follow links on demand. |
| **Full corpus** | [`/llms-full.txt`](https://docs.smartepd.com/llms-full.txt) | Every hand-written docs page concatenated into one Markdown file. Load it whole into a large context window. (Generated endpoint-reference pages aren't included — the OpenAPI spec is their machine format.) |
| **Per-page Markdown** | append `.md` to any hand-written page's path | Every hand-written page has a raw-Markdown mirror — e.g. this page is served at [`/guides/agent-integration.md`](https://docs.smartepd.com/guides/agent-integration.md) — same content, none of the navigation chrome. Generated endpoint pages aren't mirrored; use the OpenAPI spec instead. |
| **OpenAPI spec** | [`/openapi/v2.json`](https://docs.smartepd.com/openapi/v2.json) | The source of truth for endpoints: paths, request/response schemas, and status codes for the public v2 API. Feed it to tool generators, OpenAPI-to-MCP bridges, or straight into context. |
| **Per-EPD field schema** | `GET /api/v2/epds/{epd_id}/schema/?view=summary` | Runtime discovery of the exact fields, types, and requirements a *specific* EPD must satisfy. Spec-dependent — always fetch it rather than assuming. See [EPD Schema (API)](/concepts/epd-schema). |
:::note Which to choose
For most integrations: `llms.txt` + the OpenAPI spec + the copy-paste block below. Reach for `llms-full.txt` only when the agent genuinely needs deep platform background (verification workflows, constraint design), since it is large.
:::
## Ground your agent (copy-paste context)
Drop this block into your agent's context — a `CLAUDE.md` / `AGENTS.md`, a Cursor rule, or a system prompt. It compresses the platform's non-negotiables into what an agent must know before its first request:
```markdown
# Smart EPD API — agent context
Smart EPD manages Environmental Product Declarations (EPDs): draft → validate →
verify → publish. Full docs index: https://docs.smartepd.com/llms.txt
OpenAPI spec: https://docs.smartepd.com/openapi/v2.json
## Connection
- Sandbox (develop here): https://sandbox.smartepd.xyz
- Production: https://app.smartepd.com
- Every request: `Authorization: Bearer $SMARTEPD_API_KEY` (keys start with `sk_`).
- API keys are scoped to ONE workspace. Resources in any other workspace
return 404, not 403.
## Object model (top-down)
Organization → Workspace → Category (PCC; binds a PCR spec + LCA tool) →
Project → EPD (+ project-level LCA). Reference EPDs can spawn variant EPDs
that are auto-validated by constraints instead of manually reviewed.
## Golden rules
1. Discover fields before writing: GET /api/v2/epds/{id}/schema/?view=summary.
Field IDs are hyphenated (`product-name`, `declared-unit`). Never invent
field IDs — the schema endpoint is the source of truth per EPD.
2. PUT /api/v2/epds/{id}/document/ REPLACES the entire document. Always
read-modify-write; never send a partial body.
3. Two validation gates, both required before submit — passing one does NOT
imply the other: (a) SHAPE: after each PUT, read `constraint_violations`
from GET /api/v2/epds/{id}/ (required fields/types; hard-rejected at
submit as `shape_validation_error`); (b) REQUIREMENTS: loop the read-only
POST /api/v2/epds/{id}/validate/ until `is_valid: true`. Then POST
/api/v2/epds/{id}/submit/ (which also requires the lca_author_* fields).
4. Errors: non-2xx responses use {"error": {"code", "message", "details"?}}.
Branch on `error.code`, never on message text. 404 means "missing OR not
yours". Framework-level 422 (request-shape) and 401 (auth) responses skip
the envelope — branch on HTTP status there.
5. List endpoints paginate by cursor: request `?limit=100&cursor=...`,
response {"data", "next_cursor", "has_more"}. Loop until has_more=false.
6. Value formats: select fields take strings ("1 m3"), multiselects take
string arrays (["a1","a2","a3"]), rich-text fields take HTML.
7. Write sequentially, not in parallel. On 429 or 5xx, retry with
exponential backoff.
8. `force_status` / force-approve shortcuts exist for testing and are
rejected in production with a 400.
```
Everything in the block is expanded elsewhere in these docs — [Errors](/api-reference/errors), [Pagination](/api-reference/pagination), [EPD Schema](/concepts/epd-schema), and the [API Quickstart](/guides/api-quickstart).
## Authentication for agents
Use an **API key**, not a JWT — keys are long-lived, while JWTs expire on a short cadence that mid-task agents handle badly. From [Authentication & Access](/getting-started/authentication):
- Keys are created in the web UI (account settings) and shown **once**.
- Each key is scoped to a single **workspace** and to `read_only` or `read_write`. Give a drafting agent `read_write`; give a reporting/query agent `read_only`.
- Develop against the **sandbox** first. The [verification-testing shortcuts](/guides/verification-testing) (`force_status`, `/force-approve/`) let an agent run the full lifecycle end-to-end without waiting on human verifiers — and are rejected in production, so promotion to prod fails safe.
:::caution Handle keys like secrets
Inject the key via environment variable (`SMARTEPD_API_KEY`) rather than pasting it into prompts or agent config files, and never let an agent echo it into logs, URLs, or code it writes.
:::
## The draft → validate → submit loop
This is the workflow to point an agent at. The full end-to-end setup (workspace → category → project) is covered in the [API Quickstart](/guides/api-quickstart); the loop below is the part an agent iterates on for every EPD.
```mermaid
flowchart TD
A["GET /epds/{id}/schema/?view=summary discover fields + requirements"] --> B["Build document from source data (hyphenated field IDs)"]
B --> C["PUT /epds/{id}/document/ full replacement"]
C --> D["Shape gate: constraint_violations on GET /epds/{id}/"]
C --> E["Requirements gate: POST /epds/{id}/validate/ (read-only)"]
D --> F{"Both gates clear?"}
E --> F
F -- "no — fix the document" --> B
F -- yes --> G["POST /epds/{id}/submit/"]
G --> H["Verification workflow (sandbox: force-approve)"]
```
**1. Discover the schema.** `GET /api/v2/epds/{epd_id}/schema/?view=summary` returns a flat `fields` list (`id`, `type`, `required`, `label`, options, constraints) plus the requirements the EPD will be checked against. Conditional fields carry rules describing when they apply — prune fields whose conditions aren't met rather than sending them as `null`.
**2. Write the document.** `PUT /api/v2/epds/{epd_id}/document/` with the complete `document` object. Saves are deliberately permissive — incomplete documents are accepted so drafts can be built up incrementally.
**3. Clear both validation gates.** Two independent checks stand between a draft and a successful submit, and **passing one says nothing about the other**:
- **Shape gate — required fields and types.** Every document save runs a soft shape check against the schema's field definitions. Violations don't block the save; they're stored and returned as `constraint_violations` on `GET /api/v2/epds/{epd_id}/`. Read that field after each save and fix what it reports — at submit time this same check turns hard.
- **Requirements gate — spec conformance.** `POST /api/v2/epds/{epd_id}/validate/` evaluates the document against the resolved spec's requirement checks **without changing any state**, so an agent can call it as often as it likes:
```json
{
"is_valid": false,
"no_image": false,
"summary": {
"compliant": 41,
"non_compliant": 2,
"ofi": 0,
"na": 1,
"errors": 0,
"warnings": 1,
"unevaluated": 0,
"inconclusive": 0,
"pending_verification": 3
},
"requirement_results": [ /* one entry per requirement, with verdicts */ ]
}
```
`is_valid` is true when there are no non-compliant requirements and no errored checks — **on this axis only; it does not check required fields**, which is why the shape gate must be cleared separately. Treat `requirement_results` as the fix list: repair the document, re-`PUT`, re-validate. `pending_verification` counts requirements that can only be satisfied by third-party verification — an agent cannot fix these and should not loop on them.
**4. Submit.** `POST /api/v2/epds/{epd_id}/submit/` enforces its own blockers, none of which `/validate/` covers: the LCA author attribution fields (`lca_author_name`, `lca_author_email`, `lca_author_company` — set them at create time), the hard version of the shape gate (error-severity violations reject with a `shape_validation_error` whose `details` list exactly what's wrong), and — for reference and variant EPDs — template constraints. An agent that cleared both gates in step 3 and set the author fields up front should submit cleanly.
**Variant EPDs** get the same treatment: `POST /api/v2/epds/{epd_id}/variants/{variant_id}/validate/` checks a variant against its reference template's constraints without changing state. See [Variant EPDs](/guides/variant-epds) and [Reference EPDs & Constraints](/guides/reference-epds-and-constraints).
## Guardrails
Beyond the golden rules in the context block:
- **The schema endpoint is per-EPD truth.** Field sets differ by PCR spec and template, and conditional rules add/remove required fields based on other answers (e.g. the renewable-electricity block). Re-fetch the schema when switching projects or specs instead of caching one field list globally.
- **Locked fields exist.** Reference EPDs can lock methodology fields; variants that try to change a locked field get a `400 validation_error` naming it. The correct agent response is to drop the change, not retry it.
- **Don't parse opaque values.** Cursors, IDs, and error messages are not contracts. `error.code`, field IDs, and documented response keys are.
- **Respect the 404 mask.** A 404 never proves an ID doesn't exist — the same status is returned for resources the key simply can't see. Don't have an agent "probe" IDs to map what exists.
- **Keep write concurrency at 1 per EPD.** `PUT /document/` is a full replacement, so two concurrent writers silently drop each other's fields.
## Using the OpenAPI spec with agent tooling
[`/openapi/v2.json`](https://docs.smartepd.com/openapi/v2.json) describes the full public API surface and regenerates with every docs deploy, so it tracks the live contract. Common ways to wire it in:
- **Direct context** — the spec is small enough to hand to a large-context model alongside `llms.txt`.
- **Typed clients** — generate a client with [OpenAPI Generator](https://openapi-generator.tech/) and give your agent the typed SDK instead of raw HTTP.
- **MCP / tool bridges** — generic OpenAPI-to-tool bridges (including OpenAPI-to-MCP servers) can expose each endpoint as a callable tool. Smart EPD doesn't ship a first-party MCP server today; the spec is designed so bring-your-own bridges work.
## Related pages
- [API Quickstart](/guides/api-quickstart) — the end-to-end curl walkthrough an agent's workflow mirrors
- [EPD Schema (API)](/concepts/epd-schema) — full decode of the schema endpoint both views
- [Errors](/api-reference/errors) — the error envelope and code table
- [Pagination](/api-reference/pagination) — the cursor model
- [Verification Testing Shortcuts](/guides/verification-testing) — sandbox-only force options for exercising the full lifecycle
---
Source: https://docs.smartepd.com/guides/public-epd-library.md
# Public EPD Library
One anonymous endpoint lists every publicly listed EPD on Smart EPD, from **both** platforms:
- **`v2`**, the current platform.
- **`v1`**, the classic platform.
It needs no API key, no login and no headers. Use it to power a product catalogue, sync the library into your own database, or look up whether a manufacturer's declaration is still valid.
```bash
curl -s "https://app.smartepd.com/api/v2/public/epds/?q=cement&limit=5" | jq
```
:::note One list, two platforms
Smart EPD is migrating from its classic platform to the current one, so today a published EPD lives on one or the other. This endpoint merges them into a single newest-first list. Every row says which platform it came from, and links to that platform's public page for the EPD.
:::
## What is listed
An EPD is listed exactly when its own platform shows it publicly. That's the same rule its public web page applies, so a listed EPD's `url` always resolves.
| Rule | Current platform (`v2`) | Classic platform (`v1`) |
| --- | --- | --- |
| Published | Status is `published` | Status is published **and** the EPD is marked visible in the library |
| Not expired | Expiration date is today or later, or none is recorded | Same |
| Not archived | Archived EPDs are never listed | Same |
- **Expiry.** An EPD stays listed *through* its expiration date and drops out the following day.
- **Republished EPDs.** A superseded version is not listed; only the current version is.
- **Same product on both platforms.** A product can appear twice if it was re-declared on the current platform while its classic EPD is still published. These are separate declarations with separate declaration numbers.
## Request
`GET /api/v2/public/epds/`
| Query parameter | Default | Description |
| --- | --- | --- |
| `q` | — | Case-insensitive search across product name, manufacturer name and declaration number. Hyphens and spaces match each other, so `smartepd 2025` finds `SmartEPD-2025-…`. Maximum 200 characters. |
| `platform` | both | `v1` or `v2` to list one platform only. Any other value returns `422`. |
| `limit` | `25` | Page size, clamped to 1–100. |
| `cursor` | — | The `next_cursor` from the previous page. |
Requests are rate limited to **300 per minute per client IP**, which is plenty to walk the whole library (roughly ten requests at `limit=100`). Past that you get `429` with error code `rate_limited`: back off and retry. The `v2` detail endpoint below has its own, lower limit of **120 per minute per client IP**.
## Response
```json
{
"data": [
{
"id": "01928f3a-7c1e-7b4d-9a2e-5f6b8c9d0e1f",
"platform": "v2",
"product_name": "Type X Gypsum Board",
"manufacturer": "Acme Gypsum",
"declaration_number": "SmartEPD-2026-012-0042-01",
"pcr": { "name": "PCR for Gypsum Board", "version": "2.0" },
"date_of_issue": "2026-09-24",
"expiration_date": "2031-09-24",
"is_prospective": false,
"ec3_id": null,
"url": "https://app.smartepd.com/v2/epd-library/01928f3a-7c1e-7b4d-9a2e-5f6b8c9d0e1f",
"detail_url": "https://app.smartepd.com/api/v2/public/epds/01928f3a-7c1e-7b4d-9a2e-5f6b8c9d0e1f/view/"
},
{
"id": "65f0aabbccddeeff00112233",
"platform": "v1",
"product_name": "Series 100 Epoxy Coating",
"manufacturer": "Acme Coatings",
"declaration_number": "SmartEPD-2026-151-1015-02",
"pcr": { "name": "PCR for Architectural Coatings", "version": "1" },
"date_of_issue": "2026-09-23",
"expiration_date": "2031-09-23",
"is_prospective": false,
"ec3_id": "ec3a1b2c3d4e5f60718293a4b5c6d7e8",
"url": "https://app.smartepd.com/epd-library/65f0aabbccddeeff00112233",
"detail_url": "https://app.smartepd.com/api/epd-library/65f0aabbccddeeff00112233"
}
],
"next_cursor": "bTp7InYxIjogWyIyMDI2LTA5LTIzVDE0OjAyOjExIiwgIjY4ZjYz...",
"has_more": true,
"unavailable_platforms": []
}
```
| Field | Description |
| --- | --- |
| `id` | The EPD's id **on its own platform**: a UUID for `v2`, a 24-character hex id for `v1`. Key your records on the pair (`platform`, `id`), because `id` alone is only unique within one platform. |
| `platform` | `v2` or `v1`. |
| `product_name`, `manufacturer` | Product and declaring organization. `manufacturer` may be `null`. |
| `declaration_number` | The registration number printed on the EPD. |
| `pcr` | The Product Category Rules the EPD conforms to (`name`, and `version` where the PCR publishes one), or `null`. |
| `date_of_issue`, `expiration_date` | `YYYY-MM-DD`. `date_of_issue` can be `null` on older EPDs that never recorded it. |
| `is_prospective` | `true` for a prospective EPD, which is based on a forward-looking LCA and valid for one year. |
| `ec3_id` | The EPD's id in Building Transparency's EC3, when it is listed there. |
| `url` | The EPD's public web page. Link people here. |
| `detail_url` | Anonymous JSON for the full EPD. |
### Fetching the full EPD
`detail_url` points at each platform's own detail endpoint, and **the two payloads have different shapes**:
- **`v2`**: `GET /api/v2/public/epds/{id}/view/` (documented in the API Reference) returns the complete digital declaration: document, results, PCR metadata and verifier.
- **`v1`**: the classic platform's library record. It is a V1-shaped payload that the v2 OpenAPI spec does not describe.
Everything you need to list, search and link EPDs is in the summary rows above. Only follow `detail_url` when you need the full declaration.
## Ordering and pagination
The list is newest first by date of issue, across both platforms. It uses the standard [cursor envelope](/api-reference/pagination) (`data` / `next_cursor` / `has_more`), so walk it until `next_cursor` is `null`:
```bash
CURSOR=""
while :; do
PAGE=$(curl -s "https://app.smartepd.com/api/v2/public/epds/?limit=100${CURSOR:+&cursor=$CURSOR}")
echo "$PAGE" | jq -c '.data[] | {platform, id, product_name}'
CURSOR=$(echo "$PAGE" | jq -r '.next_cursor // empty')
[ -z "$CURSOR" ] && break
done
```
- **Gapless.** Unlike the `created_at` caveat on other list endpoints, this cursor never skips or repeats an EPD, even when many share a date of issue. It keeps a separate position for each platform, so each page resumes both exactly where they stopped.
- **Opaque.** Pass it back unchanged. It is only valid for this endpoint. A malformed cursor returns the first page rather than an error.
- **Tied to your query.** Keep `q` and `platform` the same for the whole walk. A cursor carried over to a different query won't fail, but that query's results can come back out of order.
- **No total count.** There isn't one. Walk the pages if you need the number.
## When a platform can't be read
`unavailable_platforms` is normally `[]`. If one platform can't be reached, for example `["v1"]` during a classic-platform outage:
- The page is served from the other platform only.
- `has_more` reflects only the platform that was read.
**Don't treat EPDs missing from that page as delisted.** If you mirror the library, skip deletions for any run where `unavailable_platforms` was non-empty and retry later. You can keep paginating, because the cursor holds the unavailable platform's place. When it comes back, later pages pick it up exactly where it left off, although those rows may be older than ones you have already received.
After an outage, Smart EPD waits about 30 seconds before trying the unavailable platform again, so `unavailable_platforms` can stay set briefly after the platform has recovered.
## Regional deployments
The EU deployment (`https://app.smartepd.eu`) serves the same endpoint for the EU library. The classic platform never ran there, so every EU row is `v2` and `unavailable_platforms` is always `[]`. The two regions are separate libraries: each lists only its own EPDs.
## Related
- [Pagination](/api-reference/pagination): the shared cursor envelope.
- [Authentication & Access](/getting-started/authentication): which endpoints need credentials.
- [EPD Lifecycle](/workflows/epd-lifecycle): publishing is what puts an EPD into this list.
---
Source: https://docs.smartepd.com/workflows/epd-lifecycle.md
# EPD Lifecycle
This page describes the end-to-end lifecycle of an EPD on Smart EPD, from initial draft through third-party verification to publication and beyond.
## Overview
Every EPD follows a structured path through six phases:
```mermaid
graph LR
SETUP["Setup"]
AUTHOR["Author"]
SUBMIT["Submit"]
REVIEW["Review"]
APPROVE["Approve"]
PUBLISH["Publish"]
SETUP --> AUTHOR --> SUBMIT --> REVIEW --> APPROVE --> PUBLISH
style SETUP fill:#6366F1,color:#fff,stroke:#4F46E5
style AUTHOR fill:#8B5CF6,color:#fff,stroke:#7C3AED
style SUBMIT fill:#F59E0B,color:#fff,stroke:#D97706
style REVIEW fill:#EC4899,color:#fff,stroke:#DB2777
style APPROVE fill:#10B981,color:#fff,stroke:#059669
style PUBLISH fill:#06B6D4,color:#fff,stroke:#0891B2
```
## Phase 1: Project setup
Before an EPD can be created, the workspace, Category, and project must be configured. See [Onboarding & Setup](/workflows/onboarding-workflow) for the full walkthrough.
## Phase 2: Author the EPD
With the project in place, the manufacturer (or consultant) creates and fills the EPD:
1. **Create a draft EPD** -Choose the EPD type based on the Category mode:
- **Standard EPD** -For manual Categories or standalone assessments
- **Reference EPD** -For tool-based Categories. Linked to an EPD Template with locked fields and validation constraints
- **Variant EPD** -Derived from an approved reference EPD with different product parameters
2. **Fill sections** -Enter data for each EPD section: general information, product description, system boundary, LCA methodology, environmental impact results, and more.
3. **Upload LCA documentation** -Attach the LCA methodology report, background data documentation, and supporting files.
### Converting to a Reference EPD
A standard draft EPD can be converted ("morphed") to a reference EPD. This links it to an EPD Template from the project's specification chain, locking certain fields and enabling variant EPD production after verification.
:::note
Only one reference EPD is allowed per project. Converting to reference is a one-way operation.
:::
## Phase 3: Submit for verification
When the EPD and LCA documentation are ready, the manufacturer bundles them into a **submittal package**: a structured collection of items that the verifier will review as a unit.
A submittal package can contain:
| Item type | Description |
|-----------|-------------|
| **LCA** | The project's LCA documentation |
| **Standard EPD** | Individual EPDs for manual review |
| **Reference EPD** | EPD linked to the tool/template for tool verification |
| **Variant EPDs** | Production EPDs derived from an approved reference |
Once all items are added, submitting the package **locks it**: no further edits until the verifier returns feedback.
## Phase 4: Verification review
A verifier is assigned to the package, and the review begins. The verifier works through each item independently, examining every section.
For each EPD section, the verifier:
- **Evaluates conformance** -marks each section as conformant, opportunity for improvement (OFI), or non-conformant
- **Adds comments** — creates discussion threads on specific findings. The EPD author can **reply to comments**, enabling back-and-forth dialogue between verifier and manufacturer to resolve findings without leaving the platform.
- **Uses AI hints** — optionally reviews AI-generated conformance suggestions with confidence levels
- **Fills the checklist** — records compliance status for each requirement in the specification (Yes / No / OFI / N/A)
```mermaid
graph TD
PENDING["Pending"]
IN_REVIEW["In Review"]
APPROVED["Approved"]
FLAGGED["Flagged"]
NEEDS_REV["Needs Revision"]
PENDING -->|"start review"| IN_REVIEW
IN_REVIEW -->|"approve"| APPROVED
IN_REVIEW -->|"flag"| FLAGGED
IN_REVIEW -->|"request revision"| NEEDS_REV
FLAGGED -->|"resolved"| IN_REVIEW
NEEDS_REV -->|"manufacturer resubmits"| IN_REVIEW
style PENDING fill:#6366F1,color:#fff,stroke:#4F46E5
style IN_REVIEW fill:#F59E0B,color:#fff,stroke:#D97706
style APPROVED fill:#10B981,color:#fff,stroke:#059669
style FLAGGED fill:#EF4444,color:#fff,stroke:#DC2626
style NEEDS_REV fill:#EC4899,color:#fff,stroke:#DB2777
```
If the verifier finds issues:
- **Flag** -Marks the item for attention while continuing the review
- **Request revision** -Returns the item to the manufacturer for corrections. The manufacturer revises and resubmits.
For more detail on the verifier's process, see [Verification Workflow](/workflows/verification-workflow).
## Phase 5: Approval gates {#approval-gates}
Items within a submittal package must be approved in a specific dependency order:
```mermaid
graph TD
LCA["LCA Approved"]
STD["Standard / Reference EPDs Can now be approved"]
REF["Reference EPD Approved"]
VAR["Variant EPDs Can now be approved"]
LCA -->|"unlocks"| STD
STD --> REF
REF -->|"unlocks"| VAR
style LCA fill:#8B5CF6,color:#fff,stroke:#7C3AED
style STD fill:#F59E0B,color:#fff,stroke:#D97706
style REF fill:#EF4444,color:#fff,stroke:#DC2626
style VAR fill:#06B6D4,color:#fff,stroke:#0891B2
```
- The **LCA must be approved** before any EPDs can be approved
- The **Reference EPD must be approved** before variant EPDs can be approved
- Once **all items** are approved, the submittal package itself can be approved
These gates ensure that foundational documentation is verified before dependent deliverables are signed off.
### Package-level actions
If the verifier wants to return the entire package (not just individual items):
- **Return for feedback** -Unlocks the package so the manufacturer can make revisions
- **Reject** -Permanently closes the package (rare; typically feedback is preferred)
- The manufacturer can **create a new revision** of the package, which copies all items with review status reset
## Phase 6: Publish
After the submittal package is approved, an admin publishes the EPD. Publication:
- Assigns an auto-generated reference number (e.g., `Smart EPD-2025-001-0001-01`)
- Sets the date of issue and a 5-year validity period
- Snapshots the PCR and template settings at the time of publication
- Makes the EPD visible in the [public EPD library](/guides/public-epd-library)
## Republishing (post-publication updates)
After an EPD is published, any changes require a **republishing** workflow:
```mermaid
graph LR
PUB["Published"]
REQ["Republishing Requested"]
DRAFT["Republication Draft"]
REVIEW["Republication Review"]
APPROVED["Approved"]
REPUB["Republished"]
PUB -->|"request"| REQ
REQ --> DRAFT
DRAFT -->|"submit"| REVIEW
REVIEW -->|"approve"| APPROVED
APPROVED -->|"publish"| REPUB
style PUB fill:#06B6D4,color:#fff,stroke:#0891B2
style REQ fill:#F59E0B,color:#fff,stroke:#D97706
style DRAFT fill:#6366F1,color:#fff,stroke:#4F46E5
style REVIEW fill:#EC4899,color:#fff,stroke:#DB2777
style APPROVED fill:#10B981,color:#fff,stroke:#059669
style REPUB fill:#06B6D4,color:#fff,stroke:#0891B2
```
Republishing preserves the same EPD reference number and creates a version lineage chain, so the full history of changes is traceable.
## Related pages
- [Onboarding & Setup](/workflows/onboarding-workflow) -How to set up the workspace, Category, and project
- [Verification Workflow](/workflows/verification-workflow) -Detailed verifier process
---
Source: https://docs.smartepd.com/guides/reference-epds-and-constraints.md
# Reference EPDs & Constraints
A **Reference EPD** is the fully populated instance that proves an [EPD Template](/concepts/epd-templating) and anchors a product family. It assigns a concrete value to **every** field and becomes the parent from which **variant EPDs** are derived — "ground zero" for the family.
What it does **not** own is the rulebook. The locked-field rules and the constraint/bound definitions that decide which inputs are valid belong to the **EPD Template** — the verified modeling envelope resolved from the specification inheritance chain (Standard spec → PCR spec → verifier review → Template). The Reference EPD is *governed by* that template and *instantiates* it with real numbers; it is the first instance validated *against* the template's constraints, not the place those constraints are defined.
This page fixes the three-way distinction first (Template vs Reference EPD vs Published EPD), then covers the operational mechanics that follow from it: constraint types, conditional application, each reference EPD's own versions of its constraints and when they can be edited, the reference EPD's own input values, validation and the submit rules, the Constraint Explorer, locked fields, the model lock recorded at approval, revising an approved reference, and how everything flows into variants.
:::note The Template owns the rules; the Reference EPD instantiates them
Constraint definitions (`range` / `statistical` / `formula`), their `severity`, their conditional `applies_if_json` gating, and the spec-level locked fields are all properties of the **Template** — the resolved output of the specification inheritance chain (Standard spec → PCR spec → verifier review → Template). The Reference EPD does not define them — it supplies the concrete values those rules are checked against. Throughout this page, "the reference's constraints" is shorthand for *the template's constraints, resolved onto the reference and inherited by every variant*.
:::
## Three entities, precisely separated
These three concepts are routinely conflated. Hold them apart along four axes — what each one **is**, what it **contains**, **when** it is verified, and whether it **can be published**.
| | **EPD Template** | **Reference EPD** | **Published EPD** |
|---|---|---|---|
| **What it is** | The verified modeling envelope and reusable form — the resolved output of the spec inheritance chain. | A fully populated EPD instance that proves the template and anchors a family. It may describe a real product *or* a representative/synthetic instance deliberately constructed to exercise the template's edge cases and boundary conditions in a single populated record. | A publication state, not a separate record type — any Reference EPD or variant EPD that has been registered and declared publicly. |
| **What it contains** | The set of **locked** fields (inherited verbatim), the set of **varying** fields (user inputs), the **constraints/bounds** on those inputs, and the declared **external input fields** (`ext__*`) the constraints can read. | Concrete values for **every** field, including its own values for the template's external input fields (`reference_inputs`); variant lineage; the `authoring_policy.locked_fields` list pinned onto *its own* variants. | Nothing new — it is a Reference EPD or a variant EPD that has been registered. |
| **When verified** | In tandem with its Reference EPD — a template cannot be verified in the abstract (nothing to check until values exist). | In tandem with its template — approving the reference simultaneously confirms the locked fields, the input bounds, and that a complete EPD comes out valid. | Already verified before publication; publishing does not re-open verification. |
| **Can it be published?** | **No** — a template is a form + rulebook, not a declaration. | **Yes, conditionally** — if it describes a real product and the team chooses to. Otherwise it stays an unpublished but still-verified anchor. | **It is the published artifact**, by definition. |
The compressed analogy:
> The **Template** is the blank form plus the rulebook for what values are allowed.
> The **Reference EPD** is that form filled in with representative numbers.
> The **Published EPD** is whatever you actually declare to the world — the reference itself, or its variants.
```mermaid
graph TD
TEMPLATE["EPD Template Blank form + rulebook Locked fields, varying fields, bounds"]
REF["Reference EPD The form filled in Every field populated"]
PUBREF["Published Reference EPD Real product, declared"]
VAR["Published Variant EPDs Derived from the reference"]
TEMPLATE -->|"populated into"| REF
REF -->|"published as itself (if real)"| PUBREF
REF -->|"or seeds"| VAR
style TEMPLATE fill:#EF4444,color:#fff,stroke:#DC2626
style REF fill:#6366F1,color:#fff,stroke:#4F46E5
style PUBREF fill:#10B981,color:#fff,stroke:#059669
style VAR fill:#10B981,color:#fff,stroke:#059669
```
## What a Reference EPD is (and is not)
A Reference EPD **is**:
- The single instance where **every** field carries a concrete value — nothing left as a range or a rule.
- The parent that variant EPDs inherit from. Variants change only the product-specific values; everything else is inherited from the reference.
- The artifact the verifier reviews to confirm the template behaves correctly end-to-end.
A Reference EPD **is not**:
- The owner of the constraint definitions. Those live on the template; the reference is *checked against* them.
- Necessarily a real product. It can be a representative, synthetic instance (see below).
- Necessarily a public declaration. It can stay an internal, verified anchor while its variants are published.
## Verified in tandem: template + Reference EPD
A template defines bounds, locked values, and a form shape — but none of that can be verified in the abstract, because there is nothing concrete to check until values exist. The **Reference EPD supplies those values**, so the template and the reference are reviewed and approved **together, in a single verification pass**.
Approving the Reference EPD therefore confirms three things at once:
1. **The locked / inherited fields** — every value variants will inherit verbatim is correct.
2. **The input bounds** — the `range` / `statistical` / `formula` constraints behave as intended against a real, fully populated EPD.
3. **A complete EPD comes out valid** — the populated instance is itself a conformant declaration.
The verifier confirms the input bounds one constraint at a time: every constraint the reference answers to is a row on its review's Constraints Review tab, and each row needs its own determination before the reference can be approved (see [How the verifier reviews the constraints](#how-the-verifier-reviews-the-constraints)). Approval also **locks the model** the reference was submitted with — the EPD tool and its version, the PCR spec version, and the template's definition — so variants can be held to it (see [Submission snapshots and the model lock](#submission-snapshots-and-the-model-lock)).
```mermaid
graph TD
RT["EPD Template Locked fields + constraints/bounds"] --> REF["Reference EPD Every field populated"]
REF --> VERIFY["Verifier review Template + Reference approved together"]
VERIFY --> ANCHOR["Verified anchor Seeds the family"]
ANCHOR --> VAR["Variant EPDs"]
style RT fill:#EF4444,color:#fff,stroke:#DC2626
style REF fill:#6366F1,color:#fff,stroke:#4F46E5
style VERIFY fill:#EC4899,color:#fff,stroke:#DB2777
style ANCHOR fill:#06B6D4,color:#fff,stroke:#0891B2
style VAR fill:#10B981,color:#fff,stroke:#059669
```
This ordering is enforced operationally: **the Reference EPD must be approved before any variant can be approved.** See the [Verification Workflow](/workflows/verification-workflow) approval gates.
## Two independent questions
A Reference EPD answers two questions that are **completely independent** of each other. Conflating them is the most common source of confusion.
### Question 1 — Real or representative?
A Reference EPD can describe:
- A **real** product — an actual product the manufacturer makes, with its true values.
- A **representative** (synthetic) product — an instance deliberately constructed to exercise the template's full range of boundary conditions and edge cases at once. It is not a product you can buy; it is a single populated instance built to stress every corner of the modeling envelope, so that verifying it verifies the whole family's allowable space.
### Question 2 — Published or not?
Independently of the above, a Reference EPD can be:
- **Published** — a real declaration registered publicly.
- **Unpublished but verified** — a still-verified reference whose only job is to prove the modeling envelope behaves correctly and to seed variants. It never goes public; the variants derived from it become the public artifacts.
These two axes are orthogonal. "Real vs. representative" describes *what the reference models*; "published vs. unpublished" describes *whether it gets declared*. **Publication is orthogonal to the reference/variant distinction** — being a reference says nothing about whether it is published, and being published says nothing about whether the published record is a reference or a variant.
## The 2×2 matrix
Crossing the two axes gives four cases. Exactly one is impossible.
| | **Published** | **Unpublished (verified)** |
|---|---|---|
| **Real** | ✅ Flagship real product that also anchors the family | ✅ Real product used as the anchor but not declared publicly |
| **Representative (synthetic)** | ❌ **Impossible** | ✅ Built to span the family's boundary conditions; verified as proof, never declared |
One-line gloss per cell:
- **Real + Published** — A real, flagship product is both declared publicly *and* the parent for the family's variants. The reference is itself a published EPD.
- **Real + Unpublished** — A real product anchors the family and is verified, but the team chooses not to declare it publicly. Its variants are what get published.
- **Representative + Unpublished** — A synthetic instance is constructed to hit the template's edge cases and verified as proof the envelope behaves. It is never published; the published artifacts are its variants.
- **Representative + Published** — **Impossible.** You cannot publish a declaration for a product that does not exist. A synthetic instance can be verified as a reference, but it can never be the public declaration; it publishes only through its (real) variants.
:::info The precise takeaway
"Reference" is about **lineage** (the parent of a family). "Published" is about **disclosure** (what goes public). The only hard rule connecting the two axes: a synthetic instance can never be the published artifact, so a synthetic reference always publishes through its variants.
:::
## Standard vs Reference EPDs
| Aspect | Standard EPD | Reference EPD |
|--------|-------------|---------------|
| Purpose | Standalone declaration for a single product | Parent declaration for a product family |
| Variants | Cannot generate variants | Can generate variant EPDs |
| Constraints | Template constraints validated against the document at submission | Template constraints checked at submission against the document **and** the EPD's own [reference values](#entering-the-reference-values), reviewed row by row by the verifier, and re-checked on every derived variant |
| Conversion | Can be morphed to reference | Already reference |
## Converting to a Reference EPD
Any draft standard EPD can be converted:
```bash
POST /api/v2/epds/{epd_id}/morph-to-reference/
```
This changes `epd_type` from `standard` to `reference`. The operation is irreversible.
## How constraints flow
Constraints originate in the **specification chain** and are resolved into the project template. The Reference EPD is then validated against that template, and the same constraints flow down to every variant — the reference is the instance the resolved template is *applied to*, never the place the constraints are defined:
```mermaid
graph TD
SPEC["PCR Spec"] --> SV["Spec Version"]
SV --> REQ["Requirements"]
SV --> FG["Field Groups"]
FG --> IFD["Input Field Definitions"]
IFD --> CON["Constraints"]
CON --> RT["Resolved Template owns the constraints + locked fields"]
RT --> EPD["Reference EPD populated instance, validated against the template"]
EPD --> VAR["Variant EPDs"]
style CON fill:#F59E0B,color:#fff
style RT fill:#EF4444,color:#fff
style EPD fill:#6366F1,color:#fff
style VAR fill:#10B981,color:#fff
```
Note the ownership: constraints resolve into the **Resolved Template** (red), and the **Reference EPD** (indigo) is the populated instance checked against them.
### Constraint types
| Type | Description | Example parameters |
|------|-------------|-------------------|
| `range` | Numeric min/max bounds with configurable inclusivity | `{"min": 0, "max": 1000, "bounds_mode": "inclusive"}` |
| `statistical` | Z-score bounds against a population distribution | `{"distribution": "normal", "mean": 50, "std_dev": 10, "threshold": 2}` |
| `formula` | Expression computes a number, compared against a bound | `{"expression": "a1 + a2 + a3", "operator": "lte", "threshold": 1000}` |
| `allowed_values` | Value must be in a fixed list of allowed strings | `{"values": ["Ecoinvent 3.9", "GaBi 2023"]}` |
The formula bound key is **`threshold`** (or `min`/`max` with the `between` operator) — not `value`, which is silently ignored. For each type's full parameter reference and the formula expression grammar, see [EPD Templating](/concepts/epd-templating#input-range-constraints).
### Constraint severity
- **error** — blocks variant approve and auto-approve. When the template enforces its constraints, it also blocks a reference EPD's submission unless the author explicitly acknowledges the failure (see [Submitting a reference EPD](#submitting-a-reference-epd)).
- **warning** — flagged but does not block
- **info** — informational only
When the template's `constraints_enforced` flag is off, nothing blocks a reference EPD's submission: its rulings are informational, and the verifier still reviews each constraint.
### Conditional constraints (`applies_if_json`)
Any constraint type can be made conditional. The `applies_if_json` field specifies when the constraint should be evaluated:
```json
{
"constraint_type": "range",
"parameters_json": {"min": 0, "max": 500},
"applies_if_json": {"field": "product_type", "operator": "eq", "value": "concrete"},
"severity": "error"
}
```
Supported `applies_if_json` operators are `eq`, `neq`, `in`, `gt`, `gte`, `lt`, `lte`, `present`, and `absent` — see the [conditional application grammar](/concepts/epd-templating#conditional-application) for the full shape, including `all`/`any` combinators.
When `applies_if_json` is null, the constraint is always evaluated.
## Reading the template to understand expected data
Before filling EPD sections, fetch the template to see what fields exist and what constraints apply. This is the template's job — it is the authoritative source for the form shape, the locked values, and the bounds the Reference EPD must satisfy:
```bash
GET /api/v2/epds/{epd_id}/template/
```
This is the **public** template endpoint. It requires a **reference** EPD (`epd_type: "reference"`) — standard EPDs return `400`, and an EPD with no attached template returns `404`. It returns the reference EPD's **current constraint version** (see [Constraint versions and the edit lock](#constraint-versions-and-the-edit-lock)): the project's shared working template while the EPD is a never-submitted draft, and the EPD's own version from its first submission on. (A project-scoped `GET /api/v2/projects/{project_id}/template/` exists but is internal — it is not part of the public API surface; build against the EPD-scoped route.)
The response contains, alongside the template's `id`, `status` and `version`:
- **`locked_fields`** and **`unlocked_fields`** — the resolved input field definitions, classified by whether variants may change them. Each entry is an object with `field_id`, `label`, `type` (text, number, decimal, select, table, etc.), `help_text`, `placeholder`, `group_id`, `order`, `is_required`, `default_value`, `options_json` (valid choices for select fields), `show_if_json` (conditional display logic), `locked`, and `locked_value` (the pre-filled value when locked). Unlocked entries additionally carry `constraint_defs` — the spec-level constraints on that field.
- **`template_constraints`** — the template-level constraint list. Each entry has `constraint_type`, `parameters_json`, `severity`, `label`, `reference_field_id` (the anchor field violations are attributed to), and optional `applies_if_json`.
- **`constraints_enforced`** — whether the constraints gate submission. When `true`, a reference EPD needs a value for every external input its constraints need, and error-severity failures block unless acknowledged. When `false`, violations are surfaced but nothing blocks.
- **`external_input_fields`** — declared tool-side inputs (IDs prefixed `ext__`) that constraint expressions can reference in addition to EPD document fields. Each has a `field_id`, `label`, `data_type` (`number`, `string`, `boolean`, `array` or `object`) and `required` flag. Each reference EPD supplies its **own** values for them (see [Entering the reference values](#entering-the-reference-values)).
- **`sample_external_payload`** — a what-if scratchpad for trying constraints (the editor's **Test Constraints** panel). It is **not** what a reference EPD is submitted or verified with, and it is not part of a version: `PUT /api/v2/epds/{epd_id}/template/sample-payload/` with `{"sample_external_payload": {...}}` saves it in any status.
- **`sections`** — the template's narrative section list (`id`, `title`, `content`, `is_required`, `order`, `spec_field_id`). These are prose sections, **not** the input field definitions above — don't conflate the two.
- **The version and its edit lock** — `reference_epd_id` (`null` for the project's working template, else the reference EPD that owns this version), `supersedes_id` (a revision: the approved version it revises), `forked_from_id` (a `v1`: the working template it was copied from), `editable`, `locked_reason` and `locked_message` (whether a `PUT` would be accepted now, and if not, the refusal it would get), `shared_draft_count` (working template only: how many draft reference EPDs share it; `null` on a reference's own version) and `approved` (`{id, version, status, approved_at}` of the version the EPD's variants answer to, or `null` before its first approval).
## Constraint versions and the edit lock
The verifier approves a reference EPD **together with** the constraints it was submitted with, so those constraints cannot keep changing afterwards. Each reference EPD therefore owns **versions** of its constraint definition (its constraints, external input fields, enforcement flag and AI sanity instructions), numbered `v1`, `v2`, and so on:
- **Draft reference EPDs share the project's working template.** Until a reference EPD is first submitted, it edits the project's working template, and every other never-submitted draft reference EPD in the project sees the same edit. This keeps "edit once for every draft" working for a family of references. The editor says so: *Shared by N draft reference EPDs*.
- **The first submit gives the EPD its own `v1`.** In the same step that moves the EPD into review, the working template is copied into the EPD's own `v1` and frozen. Its spec-level constraints are resolved with the EPD's pinned PCR spec version and spec parameters, and those parameters are pinned onto the version, so a later project or PCC change does not alter it. From then on the EPD never reads the working template again: later edits by sibling drafts do not reach it.
- **Approval puts the version in force.** Approving the reference EPD approves its version and records it as the EPD's `approved_template_id`, the version its variants are validated against. It also freezes a copy of the reference's locked-field list and document as approved, which is what variants are held to.
- **A published reference keeps its version.** See [Revising an approved reference EPD](#revising-an-approved-reference-epd).
Whether the definition can be edited depends on the EPD's status. The server decides, and every read reports it (`editable`, `locked_reason`, `locked_message`):
| EPD status | Its constraints | `PUT /epds/{epd_id}/template/` |
|---|---|---|
| `draft`, never submitted | The project's working template, shared with the other draft references | Accepted |
| `in_review` | Its own version, frozen for review | `409 template_locked`, action `in_review` |
| `flagged` | Its own version, back in draft so the author can fix what the verifier flagged | Accepted; frozen again at resubmission |
| `approved` | Its approved version, in force for its variants | `409 template_locked`, action `start_revision` |
| `draft` or `flagged`, during a revision | The revision's own draft version | Accepted |
| `published` | Its approved version | `409 template_locked`, action `start_republishing` |
Staff get no exception: a frozen version is what a verifier judged. The refusal names the way forward in `details`:
```json
{
"error": {
"code": "template_locked",
"message": "Constraints v2 are approved and in force for this EPD's variants. Start a revision to change them; v2 stays in force until the revision is approved.",
"details": [{"field": "action", "message": "start_revision"}]
}
}
```
A flagged reference EPD edits the version it was submitted with, in place: nobody else uses that version, and a verifier who flags a constraint needs the author to be able to fix it. The verifier's rows show what changed at the resubmission (see [How the verifier reviews the constraints](#how-the-verifier-reviews-the-constraints)).
### Saving from a stale copy
The working template is shared by every draft reference EPD in the project and can be edited from each of their editors and from the project's template editor, and a save replaces whole lists. To make sure a save never undoes an edit made since you loaded the constraints, send the `updated_at` you loaded as `base_updated_at` with `PUT /api/v2/epds/{epd_id}/template/` (or the project template `PUT`). If the version has been saved since, nothing is written and the save is refused:
```json
{
"error": {
"code": "template_conflict",
"message": "These constraints were changed since you opened them. Reload to see the latest version before saving.",
"details": [{"field": "updated_at", "message": "2026-09-26T15:31:02.118412+00:00"}]
}
}
```
`details` carries the version's current `updated_at`. Reload, reapply your change and save again with the new value. Every successful save returns the new `updated_at`, sections-only saves included. `base_updated_at` is optional: a save without it is not checked. The same-EPD race with a submit is handled for you: a save that arrives while the EPD is being submitted waits for the submit and then gets `409 template_locked`, so a save never reports success while the submitted version lacks it.
`GET /api/v2/epds/{epd_id}/template/versions/` lists the EPD's own versions, newest first, with the same fields as a template (`id`, `version`, `status`, `reference_epd_id`, `supersedes_id`, `forked_from_id`, `approved_at`, …). A never-submitted draft has none, so it returns `[]`. A version's `status` is `draft`, `in_review`, `approved`, `superseded` (an approved version replaced by an approved revision) or `archived` (a discarded revision).
Every EPD response also carries the version summary for a reference EPD, as `template_version` (it is `null` for other EPD types), and `approved_template_id`:
```json
{
"id": "…",
"version": 3,
"status": "draft",
"is_working_template": false,
"editable": true,
"locked_reason": null,
"locked_message": "",
"approved": {"id": "…", "version": 2, "status": "approved", "approved_at": "2026-09-20T09:12:44+00:00"},
"revises": {"id": "…", "version": 2},
"revision_open": true,
"can_start_revision": false,
"next_revision_version": null,
"can_discard_revision": true
}
```
`can_start_revision` and `can_discard_revision` say what the EPD's state allows. Whether the caller may write the EPD at all is a separate permission check. `next_revision_version` is the version number **Start revision** will create, or `null` when a revision cannot be started. Numbers are never reused: after a discarded `v3`, the next revision of an approved `v2` is `v4`.
The project-wide submit, `POST /api/v2/projects/{project_id}/template/submit/`, is retired and returns `400 endpoint_retired`: each reference EPD is submitted on its own. `POST /api/v2/templates/{template_id}/approve/` and `/reject/` refuse a reference EPD's own version with `400 use_epd_review`, because approving the reference EPD approves its version. These project and template endpoints are internal.
## Entering the reference values
A reference EPD's constraints and external input fields come from its constraint version, which draft reference EPDs share (see [Constraint versions and the edit lock](#constraint-versions-and-the-edit-lock)). The **values** for those external inputs are never shared. Each reference EPD carries its own, in `reference_inputs`, keyed by the full field ID (`ext__cement_content`). They are kept apart from the EPD's `document`, because external inputs are not form fields, and from the template's `sample_external_payload`, which is only a scratchpad. Submission, the verifier's review and the Constraint Explorer all read `reference_inputs`.
**In the editor**, open the reference EPD's **Constraints** tab. When the template enforces its constraints and declares at least one external input field, a **Reference EPD values** card, marked *this EPD only*, sits above the External Input Fields and Constraints cards. A banner above those cards names the constraint version and whether it can be edited: shared with the project's other draft reference EPDs, the EPD's own draft, frozen for review, or approved. When the version is locked, the definition cards are read-only. The card has one row per external input field, with an input matching the field's `data_type`. Beside each input is a live ruling for every constraint that reads the field: passes, fails, warning, doesn't apply, or needs a value. Constraints on the EPD's own form fields are listed under **Checked against form values** and ruled on the last saved form. The rulings come from the server's constraint engine, the same one submission uses, and they follow your unsaved edits to both the values and the constraint list. **Save** stores the values.
The values can be edited while the EPD is `draft`, `flagged` or `republishing`, the same statuses as the document. From submission onward they are frozen with the rest of the EPD, and the card shows them read-only.
:::note Template constraints are not form validation issues on a reference EPD
A reference EPD's constraints are ruled on its reference values, which the form never sees. The editor's validation-issues banner therefore does not list template constraints for a reference EPD; the Reference EPD values card is where they are ruled. Document-shape issues, such as a missing required form field or a value of the wrong type, still show there. Variant EPDs are unaffected.
:::
**Over the API**, replace the values with `PUT /api/v2/epds/{epd_id}/reference-inputs/`:
```bash
curl -X PUT https://sandbox.smartepd.xyz/api/v2/epds/$REF_EPD_ID/reference-inputs/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"reference_inputs": {"strength_class": "C30/37", "cement_content": 320}}'
```
- The body is a **full replacement**, not a merge. A key left out is removed, and so is a key sent as `null` or as blank text.
- Keys may be given with or without the `ext__` prefix. They are stored with it.
- A key that is not one of the template's `external_input_fields` is rejected with `400 validation_error`.
- Each value must match its field's `data_type`. `number` takes a finite number (not `true` / `false`), `string` takes text, `boolean` takes `true` / `false`, `array` takes a list, and `object` takes an object. A mismatch is a `400 validation_error` naming the field, and so is a number too large for a double-precision float ("out of range").
- At most 200 values and 64 KB.
- Only a reference EPD with a template accepts values; any other EPD returns `400`. So does a reference EPD outside the editable statuses.
The response is the EPD detail. Every EPD response carries `reference_inputs` (`{}` on EPDs that are not references) and `constraint_failures_acknowledged_at` (see [Submitting a reference EPD](#submitting-a-reference-epd)).
## Validating against constraints
Constraint validation checks input values against the template's constraint rules. Three public endpoints run it:
- **Reference values** — `POST /api/v2/epds/{epd_id}/reference-inputs/evaluate/` (reference EPDs only). Read-only. It rules every constraint the reference EPD answers to, spec-level and template-level, against its document and its stored `reference_inputs`, and says whether submission would refuse them. This is the one evaluation that submission, the editor's Reference EPD values card and the verifier's constraint rows all use. See [Evaluating a reference EPD's values](#evaluating-a-reference-epds-values).
- **Template dry-run** — `POST /api/v2/epds/{epd_id}/template/test-constraints/` (reference EPDs only). Read-only: feed arbitrary candidate `epd_field_values` / `external_payload` and see which constraints would fire, without saving anything. It never reads the reference EPD's stored values, so it is a what-if tool, like the editor's **Test Constraints** panel. The response also reports the template's `constraints_enforced` flag, so a client can say "would fail, but enforcement is off" instead of claiming the value will be rejected. Pass `include_report: true` to additionally receive a `rulings` entry for **every** constraint — passes and `applies_if` gate-skips included — so you can tell "passed" from "didn't apply".
- **Per variant** — `POST /api/v2/epds/{epd_id}/variants/{variant_id}/validate/` — validates the variant EPD's **document** together with the variant's **parameters** (merged in under the `ext__` prefix) against the reference's template.
(A project-scoped `POST /api/v2/projects/{project_id}/template/validate/` exists but is internal — not part of the public API surface.)
The dry-run and variant responses both carry a top-level `valid` flag, `has_blocking_errors`, and a `violations` list — but note **`valid` means different things on the two endpoints**: the dry-run sets `valid: false` when *any* violation exists (warnings and info included), while the variant endpoint sets `valid: false` only for error-severity violations (a warnings-only result is `valid: true`). Key logic off `has_blocking_errors` and per-violation `severity`, not off `valid`. Violation entries look like:
```json
{
"field_id": "gwp_a1a3",
"constraint_type": "range",
"severity": "error",
"message": "1500.0 is outside [0, 1000]",
"valid_range": "[0, 1000]",
"actual_value": 1500.0
}
```
:::note The two contracts are almost — not exactly — identical
The same six keys appear in both, but the schemas differ in which are required: the template dry-run guarantees `message` and `valid_range` (with `severity` defaulting to `"error"`), while the variant endpoint guarantees `severity` (with `message` and `valid_range` defaulting to `""`). `actual_value` is a number, string, or null on the variant endpoint and untyped on the dry-run. A shared parser should treat `message`, `valid_range`, and `severity` as optional-with-defaults on both. `field_id` is always the constraint's anchor field (`reference_field_id` for template-level constraints), including for multi-field formula expressions.
:::
:::note `validate` vs constraint validation
`POST /api/v2/epds/{epd_id}/validate/` is a **separate, requirements-level** check — it returns `{ is_valid, requirement_results, summary, ... }`, not constraint violations. Use the variant or template validate endpoints above when you want the `range` / `statistical` / `formula` results.
:::
### Evaluating a reference EPD's values
`POST /api/v2/epds/{epd_id}/reference-inputs/evaluate/` takes an optional body:
```json
{
"reference_inputs": {"cement_content": 450},
"template_constraints": null
}
```
- Omit `reference_inputs` (or send `null`) to evaluate the stored values. Pass values to preview them without saving; preview values are not type-checked or limited to declared fields, because the engine rules on whatever it is given.
- Omit `template_constraints` (or send `null`) to use the saved template list. Pass a list to evaluate unsaved constraint edits in its place: at most 50 entries, and a `formula` expression at most 500 characters and 32 brackets deep. Only a caller who can edit the EPD may pass one; anyone else, the assigned verifier included, gets `403 forbidden` and evaluates the saved template. Spec-level constraints are always included.
- `source` is `"live"` by default: the EPD's current constraint version and stored values, the author's view. Send `"submission"` to rule the definitions, enforcement, input field definitions and values of the EPD's current submission snapshot instead, which is what the verifier's constraint rows show. That is its latest submission, except for an approved or published reference, where it is the submission approval locked. Edits made since, such as a flagged author's unsaved changes, do not reach it. `reference_inputs` given with it are a what-if over the snapshot's definitions. `"submission"` cannot be combined with `template_constraints` (`400`), and any other value is a `400`. The response's `source_used` says which was ruled: an EPD with no submission snapshot falls back to `live` and says so. When it is `submission`, `snapshot_id` names the snapshot.
The response:
```json
{
"enforced": true,
"rulings": [
{
"key": "TMPL-CONSTRAINT-ext__cement_content-0",
"source": "template",
"field_id": "ext__cement_content",
"label": "Cement content within the verified range",
"constraint_type": "range",
"parameters": {"min": 250, "max": 400},
"severity": "error",
"status": "failed",
"is_violation": true,
"actual_value": 450,
"value_provided": true,
"value_source": "reference_inputs",
"message": "450.0 is outside [250, 400]",
"valid_range": "[250, 400]",
"gate_description": ""
}
],
"missing_inputs": [{"field_id": "ext__strength_class", "label": "Strength Class"}],
"blocking_failures": [{"key": "TMPL-CONSTRAINT-ext__cement_content-0", "...": "the failing ruling above"}],
"would_block": true
}
```
| Key | Meaning |
|---|---|
| `rulings[].key` | Stable ID of the constraint: `CONSTRAINT-{field_id}-{idx}` for a spec-level constraint, `TMPL-CONSTRAINT-{field_id}-{idx}` for a template constraint (`idx` is its position in `template_constraints`). It is also the ID of the constraint's row on the verifier checklist. |
| `rulings[].source` | `spec` or `template`. |
| `rulings[].status` | `passed`, `failed`, `missing_value`, `skipped_gate` (its `applies_if_json` gate is not met), `not_evaluable` or `misconfigured`. Every status except `passed` and `skipped_gate` is a violation (`is_violation: true`). |
| `rulings[].actual_value`, `value_provided` | The value the constraint read, and whether there was one. |
| `rulings[].value_source` | `reference_inputs` for an `ext__` field, `document` for any other field. |
| `missing_inputs` | Enforced templates only. External fields with no value that are marked `required`, or read by a constraint that applies, as its anchor or as a name in its `formula` expression, or compared by a constraint's `applies_if_json` gate **while the values given leave that gate undecided**. A blank gate field is not needed when the gate's outcome is already decided without it: an `all` with a branch that is already false, an `any` with a branch that is already true, or a `present` / `absent` test on the same field, which reads a blank as its answer. For example, `{"all": [{"field": "product_type", "operator": "eq", "value": "steel"}, {"field": "ext__grade", "operator": "eq", "value": "A"}]}` on an aluminium product needs no `ext__grade`, and neither does `{"all": [{"field": "ext__coating", "operator": "present"}, {"field": "ext__coating", "operator": "gt", "value": 0}]}` when `ext__coating` is blank. A constraint whose gate is not met does not make its own fields required. Listed in `external_input_fields` order. |
| `blocking_failures` | Enforced templates only. Error-severity violations, other than those that only fail because of the missing values already listed. |
| `would_block` | `true` when the template is enforced and there is a missing value or a blocking failure, meaning a plain submit would refuse these values. |
| `source_used`, `snapshot_id` | `live` or `submission` (see `source` above), and the submission snapshot ruled, when there is one. |
| `input_fields` | The external input field definitions the rulings were made with, as `{field_id, label, data_type, required}`: the current constraint version's for `live`, the submission's own copy for `submission`. A flagged author who renames, retypes or removes a field changes the `live` list only. |
## Submitting a reference EPD
`POST /api/v2/epds/{epd_id}/submit/` first freezes the EPD's constraint version for review: at a first submit it copies the shared working template into the EPD's own `v1`; for a flagged EPD or a revision it freezes the EPD's own draft version. It then runs the same evaluation on the stored reference values, against that frozen version. What happens next depends on the version's `constraints_enforced` flag:
| | Enforced | Not enforced |
|---|---|---|
| **A value is missing** — an external field marked `required`, one an applicable constraint reads, or one a constraint's gate compares while the other values leave that gate undecided (see `missing_inputs` above) | **Blocks** with `400 reference_inputs_missing`. This cannot be acknowledged: without the value there is nothing for the verifier to judge. | Does not block |
| **An error-severity constraint fails** | **Blocks** with `400 constraint_failures`, unless the submit carries `acknowledge_constraint_failures=true` | Does not block |
| **A warning or info constraint fails** | Does not block | Does not block |
Document-shape errors (`shape_validation_error`) are checked first, as for every EPD. Whatever the outcome, every ruling reaches the verifier as a constraint row on the review checklist.
A missing value names the fields:
```json
{
"error": {
"code": "reference_inputs_missing",
"message": "Enter this reference EPD's value for: Strength Class, Cement Content (kg/m3).",
"details": [
{"field_id": "ext__strength_class", "label": "Strength Class"},
{"field_id": "ext__cement_content", "label": "Cement Content (kg/m3)"}
]
}
}
```
A failing constraint returns the failing rulings as `details`, in the same shape as the evaluate endpoint's `rulings`:
```json
{
"error": {
"code": "constraint_failures",
"message": "1 constraint(s) fail with this EPD's reference values. Fix the values, or submit anyway and the verifier will decide.",
"details": [
{"key": "TMPL-CONSTRAINT-ext__cement_content-0", "status": "failed", "severity": "error", "actual_value": 450, "...": "..."}
]
}
}
```
To submit anyway, repeat the call with the acknowledgement. It is a query parameter, `false` by default, so a bodiless submit still works:
```bash
curl -X POST "https://sandbox.smartepd.xyz/api/v2/epds/$REF_EPD_ID/submit/?acknowledge_constraint_failures=true" \
-H "Authorization: Bearer $TOKEN"
```
The acknowledgement is stamped on the EPD as `constraint_failures_acknowledged_at` and shown to the verifier, who still gives every failing constraint its own determination. A submit with no failing error-severity constraint sets the timestamp back to `null`. The flag does nothing for missing values.
In the editor, a submit with missing values shows a message naming the fields and switches to the Constraints tab. Failing constraints open a dialog that lists each one with its submitted value, rule and message, and offers **Go back and fix** or **Submit anyway**.
The same rules apply when the author resubmits a flagged reference EPD. The values and the EPD's own constraint version may have changed while it was flagged, so the version is frozen again and the values are checked again, with the same codes and the same `acknowledge_constraint_failures` flag. A new acknowledgement, or none, replaces the previous round's.
Reference EPDs are always submitted one at a time: the project-wide template submit that moved every draft EPD into review at once is retired.
On a reference EPD, `constraint_violations` holds only non-blocking document-shape entries; constraint rulings live on the verifier's constraint rows. Every submit rewrites `constraint_violations` for every EPD type, setting it to `null` when there is nothing to report, so entries from an earlier editor save never carry into review.
## Exploring the input space
The **Constraint Explorer** shows where a reference EPD's constraints pass, warn or fail as its inputs change. Open it with **Explore input space**: authors find it on the Reference EPD values card, verifiers on the toolbar of the Constraints Review tab. An author explores the EPD's current constraints from their draft values. A verifier explores the constraints **as submitted** (`source: "submission"`), starting from the submitted values, so a flagged author's unsubmitted edits never change what the verifier sees; the explorer says when an EPD has no submission on record and the current constraints are used instead.
- Every numeric field a constraint reads gets a slider and a number input. The default range comes from the constraint bounds.
- Pick one field as the X axis to get a pass/fail strip, or two fields to get a heatmap. Cells show pass, warning, fail or doesn't apply, each with a pattern or glyph as well as a colour, and the legend is always visible. The current point is a marker you can drag or nudge with the arrow keys. Hovering a cell lists its values and the constraints that fail there.
- A side list gives every constraint's verdict at the current point, and a headline reports what share of the explored space passes.
- An author can copy the explored point into the Reference EPD values card with **Use these values**. It is stored only when the EPD is saved. Exploring never changes a submitted EPD.
Every cell is evaluated on the server by the same constraint engine submission uses; the explorer never approximates it in the browser. The grid comes from `POST /api/v2/epds/{epd_id}/reference-inputs/explore/`, an internal endpoint that is not part of the public API contract. It accepts the same `source` as the evaluate endpoint and reports `source_used`, `snapshot_id` and `input_fields` the same way. It takes one or two axes. Each axis must be a numeric field that some constraint reads: as its anchor, in its `applies_if_json` gate, or by name in a `formula` expression. It needs a finite `min` below `max` and 2 to 81 `steps`, for at most 6,561 cells in all. Whole-number axis values come back (and are evaluated) as integers, exactly as evaluate and submission read a value the editor sends. A grid is refused with `400` before any work when cells × the constraints an axis can change exceeds 150,000, and after 10 seconds of computing; use fewer steps. `template_constraints` follows the evaluate endpoint's rules. Each cell comes back as `p` (everything passes), `w` (a warning or info constraint fails), `f` (an error constraint fails) or `s` (no constraint applies, because every one's gate is unmet). Nothing is written.
## How the verifier reviews the constraints
While a reference EPD is in review, its verification checklist has one **constraint row** for every constraint its submission was judged against, spec-level and template alike, keyed by the same `key` as the evaluate rulings. There is also one variant governance row covering the locked-field envelope: the locked fields with their values, and the fields left free to vary. Each constraint row stores the constraint's definition and its evaluation on the submitted values: the status, the submitted value and the engine's message.
The engine suggests a determination for each row:
| Ruling | Enforced | Not enforced |
|---|---|---|
| Passes | Y | Y |
| Gate not met | N/A | N/A |
| Error-severity violation | N | OFI |
| Warning / info violation | OFI | OFI |
| No reference value submitted | N (error) or OFI | No suggestion |
The codes are the checklist's; the review page shows them as C / NC / OFI / N/A. A suggestion is never a verdict. The verifier sets each row's determination on the **Constraints Review** tab, one row at a time. Bulk determination, "Accept all AI suggestions" and Copy Review never set constraint rows. A reference EPD whose template defines any constraint cannot be approved without a checklist, and cannot be approved until every constraint row has the verifier's determination and none is **N**. See [Verification Workflow](/workflows/verification-workflow#reference-epd-constraints).
The rows mirror the **submission**, not the live template. Every submission and resubmission freezes the full rulings (each constraint's definition, its gate and its evaluation) into the EPD's submission snapshot, and the rows are built from that snapshot. The EPD's constraint version is frozen while it is in review. While it is flagged, the author can edit that version and the values, but neither reaches the rows until the EPD is resubmitted. When a resubmission changes things:
- a constraint that is new at this submission gets a new row;
- a row whose constraint definition or submitted value changed **loses its determination**, because the verifier never judged what it now shows. Its notes stay, and the row records `constraint_source.verdict_reset` = `{"previous_status": "Y", "reason": "definition_changed" | "value_changed", "at": "