Ecodex API

Search, read and match carbon emission factors from 50+ public sources, and compute product carbon footprints, programmatically.

REST · v1 api_version 2026-10-01 Base URL https://getecodex.com/api/v1

1. Overview

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.

EndpointWhat it doesBilling
POST /searchAI-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 /matchMatch 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/computeBeta. 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}/finalizeBeta. 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.svgBeta. Read a sealed deliverable, publish its public page and get its QR code.Free
GET /usageYour workspace's metered usage over a period, with the same formulas as your invoice.Free
GET /openapi.jsonThe 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.

2. Authentication

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_…

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.

3. Quick start

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_…"

Reading a search result

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.

4. Matching activity rows

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:

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.

5. Product carbon footprint (beta)

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" } }
      }
    }
  }'
  1. Compute, iterate. Read 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).
  2. Finalize. 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.
  3. Read, publish. 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.

6. Response envelope

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

7. Errors and rate limits

Every error is { "error": { "code", "message", "request_id", "details" } } with a conventional HTTP status:

HTTPcodeMeaning
400invalid_requestValidation 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…).
401invalid_api_keyMissing, invalid, revoked or expired key.
402insufficient_creditsCredit-metered workspace without enough balance (details.pool, balance, required).
402spend_cap_reachedEuro-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.
403account_inactive / account_suspendedAPI access not active, or suspended after a payment failure.
404factor_not_found / session_not_found / job_not_found / not_foundUnknown factor, or outside the sources your workspace can read (both answer 404) / unknown or expired PCF session / unknown PCF job / unknown route.
409session_compute_cap / publication_conflictThe PCF session reached its 50 computes / the public page is not in the expected state (details.code: not_finalized, already_published, not_published…).
422pcf_incompleteFinalize refused: lines are excluded from the total (details.blocking_missing_fields). Not billed.
429rate_limitedA limit was reached. Honour Retry-After.
503service_unavailable / llm_quota_exhaustedTemporary engine outage. Retry later; the call is not billed.
504engine_timeoutThe engine did not answer within 100 seconds. Not billed; retry with fewer rows or later.
500internal_errorUnexpected error. Retry; if it persists, send us the request_id.

Rate limits

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.

Retries and idempotence

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.

8. Results may change over time

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.

9. Sources and attribution

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.

10. Data processing and AI sub-processors

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:

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.

11. Pricing

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.

EndpointPrice
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}/finalizeFree (the PCF was billed at compute).
GET /pcf/jobs/{id}, publication, QR codeFree.
GET /factors/{id}Free (300 reads per hour by default).
GET /filters/{facet}, GET /usageFree.

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.

12. Roadmap

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.

13. API reference

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

Open the API reference OpenAPI document (JSON)