Search, read and match carbon emission factors from 50+ public sources, and compute product carbon footprints, programmatically.
The Ecodex API is a query API: you ask for the emission factor you need and get back a short, ranked list with the data you need to document a calculation. It is not an export channel - there is no catalogue listing or pagination. For bulk delivery of whole sources, see Ecodex Connect.
| Endpoint | What it does | Billing |
|---|---|---|
POST /search | AI-ranked search of emission factors (hybrid keyword + semantic retrieval, then AI reranking). One query per call. | Per call |
GET /factors/{id} | Read one factor by identifier (the object_id of a search result): bilingual labels, value, unit, source, version, breakdowns, source link. | Free, rate-limited |
GET /filters/{facet} | Values available for a search filter (sources, scopes, locations, units, sectors, currencies…), scoped to your sources. | Free |
POST /match | Match up to 200 activity rows (bill of materials, purchases, inventory) to emission factors in one call, with a confidence per row. | Per evaluated row |
POST /pcf/compute | Beta. Compute a product carbon footprint from a structured product model (materials, transport, manufacturing, distribution, use, end of life), iteratively. | See pricing |
POST /pcf/sessions/{id}/finalize | Beta. Seal the deliverable (PACT-aligned JSON, sha-256 content hash), stored as a PCF of your workspace. | Free |
GET /pcf/jobs/{id}, …/publish, …/republish, …/revoke, …/qr.svg | Beta. Read a sealed deliverable, publish its public page and get its QR code. | Free |
GET /usage | Your workspace's metered usage over a period, with the same formulas as your invoice. | Free |
GET /openapi.json | The OpenAPI 3.1 document - the contract this page is generated from. | Public |
The API is server-to-server only. It sends no CORS headers: an API key must never be embedded in a browser or a mobile app.
Every request carries an API key created by a workspace administrator in Settings → API of the Ecodex app. The key is shown once at creation. Send it in one of two headers:
Authorization: Bearer ecodex_sk_…
# or
X-API-Key: ecodex_sk_…
401 invalid_api_key. A workspace whose API access is suspended answers 403 account_suspended.Access is enabled per workspace. The API tab appears once API access has been activated on your workspace. If you do not see it, contact us: we activate access on request, with the usage terms, the rate limits and, for pay-per-use accounts, the monthly spending cap you choose.
Search for a factor, then read it back by identifier. Replace ecodex_sk_… with your key.
curl -s https://getecodex.com/api/v1/search \
-H "Authorization: Bearer ecodex_sk_…" \
-H "Content-Type: application/json" \
-d '{"query":"hot-rolled stainless steel","language":"en","unit_type":["mass"],"context":"BOM of an industrial machine, cradle-to-gate per kg"}'
# then, with an object_id from data.results[]
curl -s https://getecodex.com/api/v1/factors/8e8d23e6-4871-4093-8089-6cfc17920e45 \
-H "Authorization: Bearer ecodex_sk_…"
$h = @{ Authorization = "Bearer ecodex_sk_…" }
$body = '{"query":"hot-rolled stainless steel","language":"en","unit_type":["mass"]}'
$r = Invoke-RestMethod -Method Post -Uri "https://getecodex.com/api/v1/search" -Headers $h -ContentType "application/json" -Body $body
$r.data.results | Format-Table name, fe, unit, source, confidence
$id = $r.data.results[0].object_id
Invoke-RestMethod -Uri "https://getecodex.com/api/v1/factors/$id" -Headers $h | ConvertTo-Json -Depth 10
import requests
API = "https://getecodex.com/api/v1"
headers = {"Authorization": "Bearer ecodex_sk_…"}
r = requests.post(f"{API}/search", headers=headers, json={
"query": "hot-rolled stainless steel",
"language": "en",
"unit_type": ["mass"],
"context": "BOM of an industrial machine, cradle-to-gate per kg",
})
r.raise_for_status()
best = r.json()["data"]["results"][0]
print(best["name"], best["fe"], "kgCO2e per", best["unit"], "-", best["source"])
factor = requests.get(f"{API}/factors/{best['object_id']}", headers=headers).json()["data"]
print(factor["name"]["en"], factor["version"], factor["source_link"])
Each entry of data.results[] carries object_id, name, fe, unit, source, scope, location, year, data_type, quality_score (0–1) and confidence (high ≥ 0.80, medium ≥ 0.50). fe is expressed in kgCO2e per unit: a result with fe: 2.18 and unit: "kg" reads 2.18 kgCO2e per kg. When you asked for a display conversion (currency, convert_to_unit), source_fe / source_unit carry the catalogue value.
Filters are exact values: discover them with GET /filters/{facet} rather than guessing (source, scope, location, unit, unit_type, data_type, sector, sub_sector, currency, convert_to_unit_targets). location is a soft preference - a better match from another location can be returned, flagged by the location_mismatch warning. The other filters are strict. The optional context (a sentence on your use case) guides the ranking.
POST /match maps up to 200 rows to emission factors in one synchronous call. config.context (50 characters minimum) is required: describe the batch - sector, product types, geography, intended use, life-cycle boundary - it drives the scoring of every row. Each row needs a unique id (your join key), an activity label and a unit.
curl -s https://getecodex.com/api/v1/match \
-H "Authorization: Bearer ecodex_sk_…" \
-H "Content-Type: application/json" \
-d '{
"config": { "context": "Purchases of a furniture maker, French labels, quantities in kg" },
"language": "en",
"items": [
{ "id": "row-12", "activity": "Panneau MDF 18 mm", "unit": "kg", "location": "France", "item_type": "raw_material" },
{ "id": "row-13", "activity": "Vis acier zinguée", "unit": "unit", "item_type": "finished_product" }
]
}'
data.items[] comes back in the order sent, one entry per id:
status: "matched" - match carries the factor (object_id, fe in kgCO2e per unit, source, scope, location, year, quality_score), confidence is high (≥ 0.75) or medium (≥ 0.50), and alternatives lists the other candidates.status: "unmatched" - the row was evaluated and no factor was acceptable; reason says why. Billed like a match.status: "error" - the row was not evaluated (error_code: timeout, rate_limited, llm_error, llm_quota_exhausted). Not billed: re-send only those rows.Pinning: any object_id from /match or /search can be read with GET /factors/{id} and pinned in a PCF (fe_objectID). config.currency / config.convert_to_unit convert the displayed value (source_fe / source_unit then carry the catalogue value); they never change the scores.
The call answers within 100 seconds: rows the engine could not evaluate in time come back in error (timeout), and a call that cannot answer at all fails with 504 engine_timeout - not billed. For large files, send batches of 200 sequentially.
The PCF endpoints run the engine of the Ecodex PCF builder on a structured product_model. Beta: the schema may still evolve (additive changes first, announced by email). Minimum model: a functional unit and the bill of materials (name, quantity, unit, production location of each material). Transport (A2), manufacturing (A3), distribution (A4), use (B) and end of life (C) are optional, with documented defaults - the final assembly of the components (A3) is modelled by default, send "A3_manufacturing": {"assembly": false} to leave it out; every gap is listed in missing_fields[] and every default in assumptions[].
curl -s https://getecodex.com/api/v1/pcf/compute \
-H "Authorization: Bearer ecodex_sk_…" \
-H "Content-Type: application/json" \
-d '{
"language": "en",
"session_id": null,
"product_model": {
"functional_unit": { "amount": 1, "unit": "unit" },
"production": { "country": ["France"] },
"stages": {
"A1_materials": [
{ "name": "Hot-rolled steel", "quantity": 2.4, "unit": "kg", "location": "Germany" },
{ "name": "Polypropylene", "quantity": 0.6, "unit": "kg", "location": "China" },
{ "name": "Corrugated cardboard", "quantity": 0.3, "unit": "kg", "role": "packaging" }
],
"A2_transport": { "legs": [ { "mode": "road", "distance_km": 800, "origin": "Germany", "dest": "France" } ] },
"A3_manufacturing": { "assembly": { "category": "mechanical", "energy_country": "France" } }
}
}
}'
data.pcf (totals per stage), data.lines, missing_fields[] and assumptions[], enrich the model and compute again. Always send back the data.session_id of the previous response - the returned id can differ from the one you sent, keep the latest. With it, unchanged materials are neither re-matched nor billed again and the total stays reproducible; without it, every call is a new, fully billed session. A session lives 30 days after its last use and accepts 50 computes (409 session_compute_cap beyond).POST /pcf/sessions/{session_id}/finalize with the same product_model seals the deliverable: a PACT-aligned footprint (deliverable.pact), a sha-256 content_hash and signature.status: "unsigned". The response carries a job_id: the deliverable is stored as a PCF of your workspace (also visible in the Ecodex app), and finalizing the same session again updates that job instead of creating a new one. A PCF with lines excluded from the total (incomplete: true) is refused with 422 pcf_incomplete and nothing is billed.GET /pcf/jobs/{job_id} returns the stored deliverable. POST /pcf/jobs/{job_id}/publish (body {"confirmed": true}) opens its public page https://getecodex.com/p/<token> - footprint and composition, never the factors, their sources or values; /republish publishes a newer deliverable as a new version of the same page, /revoke takes it offline, GET …/qr.svg returns the QR code of the active page.PDF and Excel renderings of the deliverable are not part of the API: build them from the JSON with your own branding.
Every success is { data, meta, warnings }:
{
"data": { "query": "acier", "results": [ … ] },
"meta": {
"request_id": "req_218327a9c6e24772a778a4f960c9bbaa",
"api_version": "2026-10-01",
"engine": { "name": "search", "version": "2.6.0", "model": "gemini-3.1-flash-lite", "model_version": "…", "degraded": [] },
"billing": { "billable_units": 1, "unit": "search_call" }
},
"warnings": [ { "code": "mixed_products", "message": "Results describe different products or activities." } ]
}
meta.request_id (also in the X-Request-Id header) identifies the call - quote it when you contact support.meta.billing is what this call metered, the same value as on your invoice (unit: search_call, match_item, pcf_line / pcf_finalize for euro-invoiced PCF, pcf for credit-metered workspaces, which also get credits_debited). A call the engine could not evaluate is not billed.meta.engine.degraded lists pipeline stages that failed during the call (a retrieval branch down → results ranked from a partial pool). Empty when nominal.warnings[] never blocks a response. Search: mixed_units, mixed_scopes, mixed_sources, mixed_years, mixed_products, location_mismatch, no_high_confidence_match, degraded_retrieval. Match: degraded_retrieval, prep_degraded, error_items, llm_quota_exhausted, scope_no_match. PCF: degraded, incomplete, deliverable_not_persisted. Messages are in English; send Accept-Language: fr for French.Cache-Control: private, no-store.Every error is { "error": { "code", "message", "request_id", "details" } } with a conventional HTTP status:
| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_request | Validation failed. details.code names the cause (validation_failed with details.fields[], batch_not_supported, unknown_currency, unknown_convert_to_unit, unit_not_convertible, context_required, duplicate_ids, sources_not_accessible, unknown_unit_type, invalid_json…). |
| 401 | invalid_api_key | Missing, invalid, revoked or expired key. |
| 402 | insufficient_credits | Credit-metered workspace without enough balance (details.pool, balance, required). |
| 402 | spend_cap_reached | Euro-invoiced workspace: the call could take the calendar month (UTC) above your monthly spending cap. Refused before anything runs, not billed. details: cap_cents, spent_cents, required_cents (worst case of this call), currency, period_from. Do not retry: ask us to raise the cap, or wait for the next month. |
| 403 | account_inactive / account_suspended | API access not active, or suspended after a payment failure. |
| 404 | factor_not_found / session_not_found / job_not_found / not_found | Unknown factor, or outside the sources your workspace can read (both answer 404) / unknown or expired PCF session / unknown PCF job / unknown route. |
| 409 | session_compute_cap / publication_conflict | The PCF session reached its 50 computes / the public page is not in the expected state (details.code: not_finalized, already_published, not_published…). |
| 422 | pcf_incomplete | Finalize refused: lines are excluded from the total (details.blocking_missing_fields). Not billed. |
| 429 | rate_limited | A limit was reached. Honour Retry-After. |
| 503 | service_unavailable / llm_quota_exhausted | Temporary engine outage. Retry later; the call is not billed. |
| 504 | engine_timeout | The engine did not answer within 100 seconds. Not billed; retry with fewer rows or later. |
| 500 | internal_error | Unexpected error. Retry; if it persists, send us the request_id. |
Limits are per workspace and configured with your access. Defaults: 100 calls per minute across all endpoints, plus a per-endpoint budget (search and match: hourly and daily; PCF: 60 per hour and 300 per day shared by compute and finalize; factor reads: 300 per hour). A 429 carries Retry-After (seconds, an upper bound) and the RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and RateLimit-Policy headers. Every 4xx/5xx counts toward the limits (an error loop is bounded) but is never billed - back off on 429 instead of retrying immediately.
Search calls are read-only and safe to retry after a network failure; a retried call is metered again. The same holds for /match: a call replayed after a network error is billed again, so re-send only the rows you did not receive, and only the rows in error after a partial answer. PCF computes are safe to replay with the same session_id (cached materials are not billed twice); finalizing the same session again is free. Retry on 429 after Retry-After, on 503 / 504 after a few seconds, and never in a tight loop on llm_quota_exhausted. Idempotency keys are on the roadmap.
Search, match and the material matching of the PCF are ranked by an AI reranker. For an identical request, results can change when the engine evolves (new sources imported, prompts and guards updated, model version). meta.engine.version and meta.engine.model_version let you trace which engine produced a result. If your application needs a stable reference, store the object_id you selected and read it back with GET /factors/{id}; a factor's factor_lineage_key stays stable across versions of its source, and versioned_key pins the version served.
The contract itself is versioned by the /v1 prefix and the dated meta.api_version. Additive changes (new optional fields, new warnings) keep the version; a breaking change ships as /v2 with /v1 maintained for at least six months and announced by email.
Your workspace reads the public sources plus the sources assigned to it. Licensed databases are never exposed through the API without a licence on your side. GET /filters/source lists every source with has_access and its usage rights (licence_tier: open, open_attribution, copyleft, restricted, licensed, unclear; plus licence, attribution and usage_rights_url when you pass ?source=). A source filter on /search, /match or /filters/{facet} must name sources your workspace can read: any other name refuses the call with sources_not_accessible and the list in details.inaccessible, never a silent widening to your other sources.
The source of a factor must be attributed in any display or document that uses its value (the source field, and the attribution credit line when the source publishes one). Copyleft sources (share-alike) bind any redistributed file to the same licence; restricted, licensed or unclear sources must be checked before values are shared with a third party. Values are to be used in your own calculations: the API is not a licence to rebuild or redistribute the catalogue.
Your data is stored in the European Union (Supabase on AWS, eu-west-3, Paris). AI providers are called under paid API terms with a contractual commitment not to train on submitted data:
query text (/search) or the row and material labels (/match, /pcf), to retrieve semantically close factors.query and the optional context (/search), the row labels, units, locations and config.context (/match), the material labels of the bill of materials and the matching context (/pcf), with the candidate factors, to rank them. Processing is not regionalised.Quantities, the rest of the product model and the deliverable never reach an AI provider; /factors, /filters, /usage and the PCF job endpoints involve no AI call at all. Keep labels and contexts to what the matching needs: product or activity names, units, locations, a sentence on the use case - no customer names, prices, order numbers or personal data. Request metadata (endpoint, parameters, token counts, timing) is kept for billing and service monitoring. The full list of sub-processors and our security practices are on the Trust Center.
Pay-per-use, in euros, no fixed fee, invoiced monthly on actual usage. Credit-metered workspaces (sandbox and in-app plans) use the same calls against their included credits instead.
| Endpoint | Price |
|---|---|
POST /search | €0.05 per call (one query per call). A call answered 503 is not billed. |
POST /match | €0.01 per evaluated row, matched or unmatched. Rows in error (not evaluated) are not billed. |
POST /pcf/compute | €1.00 per PCF: one PCF per session covers up to 50 distinct resolved lines (materials, shipping packaging, use-phase consumables, pinned factors included; 51 to 100 lines = 2 PCF). Billed at the first compute that resolves a line; recomputing the same session is free, up to its 50 computes. A session-less call is a new PCF. |
POST /pcf/sessions/{id}/finalize | Free (the PCF was billed at compute). |
GET /pcf/jobs/{id}, publication, QR code | Free. |
GET /factors/{id} | Free (300 reads per hour by default). |
GET /filters/{facet}, GET /usage | Free. |
Volume commitments are priced on request. GET /usage returns, at any time, the metered calls of the period and - for euro-invoiced workspaces - the amount, computed with the invoice's own formulas. Pay-per-use workspaces set a monthly spending cap with us at activation: before each billable call, the amount already spent this calendar month (UTC) plus the worst-case cost of the call (every row billable, no grace) is compared with the cap, and the call is refused with 402 spend_cap_reached when it would exceed it. Free endpoints are never capped.
Planned on the same base URL and envelope, announced by email to API customers before release:
The same engines are also available through the Ecodex MCP server, for assistant-driven use.
The full interactive reference - every endpoint, request and response schema, code samples in curl, Python, Node.js and more, and a Test request panel that sends real calls with your key (kept in the page's memory only; calls are metered like any other).