Skip to main content

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.

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:

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.

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 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:

curl -s https://sandbox.smartepd.xyz/api/v2/workspaces/ \
-H "Authorization: Bearer $TOKEN" | jq -r '.data[0].id'

Save it as $WS_ID.

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 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:

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:

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.

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 —

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.

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": "<SPEC_ID>",
"preapproved_tool_id": "<TOOL_ID>"
}'
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.

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.

curl -X POST https://sandbox.smartepd.xyz/api/v2/pccs/$PCC_ID/activate/ \
-H "Authorization: Bearer $TOKEN"

4. Create a project​

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:

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:

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:

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 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.

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": "<p>Mass-based allocation for co-products.</p>",
"cut-off-procedure": "<p>Flows below 1% of total mass excluded per EN 15804+A2.</p>",
"product-flow-diagram": "https://placehold.co/800x400?text=Flow+Diagram",
"lifecycle-stage-description": "<p>A1: Raw materials. A2: Transport. A3: Manufacturing.</p>",
"data-quality-discussion": "<p>Primary data from 2024 production year.</p>",
"limitations-text": "<p>Valid only for described products and processes.</p>",
"references": "<p>ISO 14025:2006, ISO 21930:2017, EN 15804+A2:2019</p>",
"information-modules": ["a1", "a2", "a3"],
"impacts": [
{"category": "GWP-total", "unit": "kg CO2 eq", "lca_method": "IPCC AR5 GWP 100", "A1": 250.0, "A2": 35.5, "A3": 35.0}
],
"resource_uses": [
{"category": "PERE", "unit": "MJ", "A1": 45.0, "A2": 2.5, "A3": 8.0}
],
"output_flows": [
{"category": "HWD", "unit": "kg", "A1": 0.01, "A2": 0.002, "A3": 0.005}
],
"additional_results": [
{
"discussion": "<p>Reported in addition to the PCR-required set.</p>",
"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}
}'
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 for the full contract. Omit the key entirely if you have nothing extra to report.

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. 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., "<p>Content here.</p>").

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:

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 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:

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"
]
}
}'
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.

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.

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:

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 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:

# 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.

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 for all force-approve options. Force shortcuts are rejected with a 400 in production.

12. Submit EPD for verification​

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.

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.

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.

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.

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"}'
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.)

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.
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:

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):

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​