Guide API reference

Sustained Product Footprint API — integration guide

The companion to the API reference. The reference tells you what every field is; this tells you how the API thinks, which is the part that decides whether an integration goes smoothly.

Supersedes the Sustained High-res / Low-res Product Footprint API PDFs. Where the two disagree, this is correct — see §5 for one case worth checking in existing code.


1. The model

Entity What it is
Integration Your connection to Sustained. Owns customers. One API key, one integration.
Customer A scope of data for a single end customer. Owns workspaces.
Workspace A namespace isolating products and reports — typically a brand or an environment.
Product One SKU model.
Report The environmental impact result for a product: physical quantities, scores and aggregates.

If you are a platform serving many brands, each brand is a customer; their environments are workspaces.


2. Authentication and tenancy

Send your API key on every request:

x-api-key: <your key>

The key identifies your integration. To choose which customer and workspace a request targets, add:

Header Effect
X-Integration-Customer-Alias Selects the customer. Globally unique.
X-Integration-Customer-Name Display name, applied only when the customer is first created.
X-Integration-Workspace-Alias Selects the workspace. Unique per customer. Ingestion only.

Aliases are yours to choose. Use your own customer identifiers — you do not need to store Sustained IDs to route data.

Unknown aliases are provisioned automatically. The first request carrying a new customer alias creates that customer; the same is true of workspaces. There is no separate onboarding call.

Omit an alias and the integration's default customer or workspace is used. That is convenient for a single-tenant integration and a trap for a multi-tenant one: a missing header does not fail, it writes to the default. If you serve multiple customers, always send the alias.

You can only reach workspaces created by your integration or explicitly onboarded to it.


3. How ingestion works

This is the part that is unlike most APIs, and the part worth reading twice.

Send your vocabulary, not ours

Every scalar is a three-part envelope:

{
  "category": { "source": "Dairy alternatives", "target": "cat.dairy_alt", "tag": "exact_match" }
}
Field Who sets it Meaning
source You Your value, in your own words
target Sustained The resolved Sustained entity
tag Sustained How confident the match was

You normally send only source. Sustained's mapping layer resolves target during ingestion, and returns the completed document when you read the product back.

The point: you do not have to adopt Sustained's taxonomy, pre-map your catalogue, or maintain a translation table. Send "Dairy alternatives", or "DAIRY-ALT", or whatever your PLM calls it, and it resolves.

If you already know the Sustained identifier, set target yourself and it is used as-is.

Read the tags before you trust the number

Every resolved field comes back with a tag:

Tag What it means
exact_match High-confidence exact match
best_match Plausible match, medium confidence
low_quality_match Low confidence. Mapped and included, but worth reviewing
default You sent nothing, so a Sustained default was applied
unmatched No match found. Excluded from the assessment entirely
user_input Taken as given, no matching applied

unmatched is the one to watch: that ingredient contributes no impact at all, so the footprint is understated rather than wrong-looking. A sensible integration surfaces unmatched and low_quality_match counts to whoever owns the data.

report.firstPartyData gives you the same signal in aggregate — the percentage of the result derived from what you sent, rather than from defaults.

Sensible defaults, not failures

Where you leave things out, Sustained fills them in rather than rejecting the product. The result is a complete assessment with a lower firstPartyData score.

Suppliers — for ingredients, packaging and facility owners:

Facilities — production, storage, retail, consumption and final destination:

Note the product-type asymmetry: retail and consumption belong to final products, final destination belongs to internal products.


4. Choosing a resolution

Low-res High-res
For Screening a wide catalogue Decision-grade modelling
Ingredients, materials, production facilities Yes Yes
Packaging Rejected Yes
Distribution paths Rejected Yes
Product groups Rejected Yes
Ingredient loss Rejected Yes
Facility emissions and waste Rejected Yes
Report Score + categories Categories, contributor tree, transport, consumer label

Rejected means 400, not ignored. Sending packaging to a low-res endpoint fails the whole request with packaging is not supported in low resolution use-case. This is deliberate — silently dropping half a product document produces a plausible, wrong number.

The two are separate worlds. A SKU ingested at low resolution cannot be read through the high-res API: you get 401, not 404. To change resolution, delete and re-ingest.


5. Ingestion is asynchronous

A successful POST returns 201 with a job, not a report:

{
  "id": "PW1002",
  "status": "queued",
  "customerId": "customerid.f238625e01f845fabd1100585225af06",
  "workspaceId": "workspace.33163c24ac1241fa8649e811d57b8a9f",
  "createdAt": "2026-08-07T09:04:11Z",
  "updatedAt": "2026-08-07T09:04:11Z"
}

Poll the matching GET until status settles:

queued → mapping → calculating → finished
                              ↘ failed
Status Meaning
queued Accepted, not started
mapping Resolving your source values to Sustained entities
calculating Running the impact calculation
finished Done. report is populated
failed Failed. error explains why

product appears once mapping completes, so you can inspect the resolved document — and its tags — before the report exists.

Once finished, check productState:

Note the spelling: complete, not completed. Earlier PDF revisions of this document said completed in prose while the API has always returned complete. If you have a comparison against "completed", it has never matched.

Poll politely. Back off between attempts rather than tight-looping — see §7.


6. Working with SKUs

skuId.source is your identifier and the key for everything afterwards. It is unique per customer, across all of that customer's workspaces.

On the second form, skuId.source in the body must equal the path segment. A mismatch is a 400, not a silent overwrite of whichever one you meant.

Deleting a product that another product depends on — an internal or intermediate product used as an ingredient elsewhere — returns 409 with errorCode: resource-in-use. Delete the dependants first.


7. Errors

All errors share one shape:

{ "message": "distribution paths are not supported in low resolution use-case",
  "response_code": 400 }

Some carry an errorCode for programmatic handling.

Status When What to do
400 Validation failure, SKU mismatch, unknown category, low-res restriction Fix the request. Retrying will not help
401 Bad API key, or reading a SKU at the wrong resolution Check the key, or use the other resolution's endpoint
404 No such SKU for this customer Check the alias headers — you may be looking in the default customer
409 Product is in use by another product Delete the dependants first
429 Throttled Back off exponentially with jitter, then retry
500 Our fault Retry with backoff. Quote X-Sustained-Request-Sid if it persists

message names what actually failed — read it rather than pattern-matching on the status code alone.

Every response carries an X-Sustained-Request-Sid header. Log it. It is the fastest route to an answer from support.


8. Quickstart

Low-res — screening

# 1. Ingest
curl -X POST https://api.sustained.com/impact/v1/ingest/lowres \
  -H "x-api-key: $SUSTAINED_API_KEY" \
  -H "X-Integration-Customer-Alias: acme" \
  -H "Content-Type: application/json" \
  -d '{
    "skuId":    { "source": "PW1002" },
    "name":     { "source": "Oat Drink Barista 1L" },
    "amount":   { "source": "1000", "target": 1000 },
    "unit":     { "target": "ml" },
    "category": { "source": "Dairy alternatives" },
    "ingredients": [
      { "name": { "source": "Oats" },  "amount": { "target": 100 }, "unit": { "target": "g" } },
      { "name": { "source": "Water" }, "amount": { "target": 880 }, "unit": { "target": "g" } }
    ]
  }'

# 2. Poll until status is finished
curl https://api.sustained.com/impact/v1/ingest/lowres/PW1002 \
  -H "x-api-key: $SUSTAINED_API_KEY" \
  -H "X-Integration-Customer-Alias: acme"

# 3. Or fetch just the headline score
curl https://api.sustained.com/impact/v1/ingest/lowres/PW1002/impact-score \
  -H "x-api-key: $SUSTAINED_API_KEY" \
  -H "X-Integration-Customer-Alias: acme"

High-res — add packaging and distribution

Same shape, plus the fields low-res rejects:

{
  "skuId":    { "source": "PW1002" },
  "name":     { "source": "Oat Drink Barista 1L" },
  "amount":   { "source": "1000", "target": 1000 },
  "unit":     { "target": "ml" },
  "category": { "source": "Dairy alternatives" },
  "ingredients": [
    { "name": { "source": "Oats" },  "amount": { "target": 100 }, "unit": { "target": "g" },
      "loss": { "target": 2 } }
  ],
  "packaging": [
    { "name":   { "source": "Carton 1L" },
      "amount": { "target": 28 },
      "unit":   { "target": "g" },
      "materials": [
        { "category": { "source": "Paper" },   "share": { "target": 75 } },
        { "category": { "source": "Plastic" }, "share": { "target": 25 } }
      ] }
  ],
  "distributionPaths": [
    { "storageFacilities": [ { "location": { "source": "United Kingdom" } } ],
      "retailFacility":      { "location": { "source": "United Kingdom" } },
      "consumptionLocation": { "location": { "source": "United Kingdom" } } }
  ]
}

Then break down what drives the result:

curl "https://api.sustained.com/impact/v1/ingest/highres/PW1002/contributors?category=pef_climate_change" \
  -H "x-api-key: $SUSTAINED_API_KEY" \
  -H "X-Integration-Customer-Alias: acme"

category is required. Add &includeOnlyMajor=true to prune the tree to the significant contributors.


9. Reading a high-res report

Contributor nodes that map to another product carry reference.reportApiUrl, letting you walk into a nested product's own report.


10. Taxonomies

Four endpoints return the accepted vocabularies:

Endpoint Use for
GET /v1/ingest/categories category.source on a product
GET /v1/ingest/packaging packaging types, material categories and material types
GET /v1/ingest/production-steps/food-production processId on a food production step
GET /v1/ingest/production-steps/packaging-production processId on a packaging step
GET /v1/ingest/packing-steps/food-packing processId on a packing step

These are authoritative and change without an API version bump. Read them rather than hard-coding — and remember you do not have to use them at all, since source accepts your own vocabulary. They are most useful for building a mapping UI or for pre-validating input.

Processes flagged placeholder: true are usable stand-ins that contribute a generic impact rather than a specifically modelled one.