Everything an enforcement agency or platform partner needs to reproduce the EUDR Enforcement Dashboard using the Epoch SCO2 API.
The Enforcement Dashboard provides three complementary workflows for EUDR compliance monitoring, exposed as tabs:
All data is fetched from the Epoch SCO2 REST API. The dashboard is a static HTML page — no backend required beyond the API.
All API requests require a Bearer token. Obtain one by calling the /refresh_token endpoint with user credentials in a JSON body:
POST https://epoch-sco2-api.com/refresh_token
Content-Type: application/json
{ "email": "USER", "password": "PASS" }
Response:
{ "access_token": "eyJhbGciOiJSUzI1NiIs..." }
Include the token in all subsequent requests:
Authorization: Bearer <access_token>
x-internal-api-key and x-user-id headers instead of Bearer tokens. Contact Epoch to receive your API key.
Base URLs:
https://epoch-sco2-api.comhttps://dev-epoch-sco2-api.comReturns all producers/facilities as streaming NDJSON. Each line is a GeoJSON Feature with compliance metrics and metadata. This is the primary endpoint for loading the Country Commodity Assessment tab.
| Parameter | Value | Purpose |
|---|---|---|
stat_type | "all" | Return all supply sheds regardless of assessment type |
table_extension | "eudr" | Query the EUDR-specific results table |
limit | 100000 | Max rows (omit for all) |
GET /fetch_supply_shed?stat_type=all&table_extension=eudr&limit=100000
| Field | Type | Description |
|---|---|---|
collection_id | string | Unique identifier for this assessment |
company_name | string | Producer/supplier name |
commodity | string | Commodity type (rubber, timber, palm_oil, etc.) |
country | string | Facility country |
admin1 | string | Admin level 1 region (province/state) |
locations_confidence | string | high/medium/low = assessed; anything else = skip reason |
noncompliance_plot_count | int | Number of plots with critical deforestation alerts |
noncompliance_area_ha | float | Total non-compliant area in hectares |
noncompliance_area_perc | float | Non-compliant area as fraction of total area (0.0 to 1.0). Used for risk classification. |
noncompliance_area_rate | float | Annualized non-compliance rate |
area_ha_supply_shed | float | Supply shed area (> 0 = supply shed assessment) |
stat_type | string | Assessment type (contains supply_shed if supply shed) |
legality_risk | string | Article 10 risk: Low / Medium / High |
ingestion_date | timestamp | When the assessment was processed |
metadata | JSON string | Custom metadata (supplier IDs, part IDs, contact emails) |
Returns plot-level deforestation results for a specific producer. Used for the drill-down detail view when a user clicks a row in the aggregate table.
| Parameter | Value | Purpose |
|---|---|---|
aggregate | false | Return individual plots (not aggregated) |
limit | 100000 | Max plots |
filename | e.g. "0x49a7..." | Collection ID to fetch plots for |
GET /fetch_deforestation_check?aggregate=false&limit=100000&filename=0x49a7...
| Field | Type | Description |
|---|---|---|
uuid | string | Unique plot identifier |
area | float | Plot area in hectares |
noncompliance_area | float | Deforested area in hectares |
noncompliance_area_perc | float | Deforestation percentage of plot area |
deforestation_alert | string | "critical", "non-critical", or "no deforestation" |
deforestation_confidence | string | Detection confidence (very high, high, medium, low, very low) |
geometry | GeoJSON | Plot polygon |
Runs real-time location screening on uploaded GeoJSON, CSV, or XLSX files. Used by the DDS Screening and Producer Screening tabs. Supports both plot polygons and facility points. CSV/XLSX files are automatically geocoded via the Google Geocoding API.
The assessments are toggled by a small set of grouped, EUDR-oriented flags (all default false). Turn on only what you need — each adds latency and cost.
| Parameter | Value | Purpose |
|---|---|---|
file | GeoJSON / CSV / XLSX | Location geometries to screen (multipart/form-data body) |
location_type | "plot", "facility", or "shed" | Type of locations being screened |
commodity_type | e.g. "palm_oil" | Expected commodity (used for the commodity-presence check). If omitted, auto-detect is attempted. |
country | ISO-2, e.g. "CI" | Country context (drives the Article 29 country-tier routing gate) |
include_art9 | true | Article 9 assessment — deforestation + commodity presence. Plots: per-plot 30 m check. Facilities: 50 km radius, natural forest only, post-2020 cutoff. |
include_art10 | true | Article 10 procedural-risk assessment (the canonical Article 10(2) rubric, criteria a–j). Uses AI web search; sources ranked by relevance then freshness. |
include_legality | true | Legality / adverse-media check (the legality_risk_* fields). include_legality_check is accepted as a backward-compatible alias. |
include_hazard | true | Natural-hazard assessment (flood, fire, etc.) |
include_infrastructure | true | Infrastructure & single-point-of-failure assessment |
check_road_access | true | (Facility only) Check proximity to road networks |
check_water_access | true | (Facility only) Check proximity to waterways |
check_port_proximity | true | (Facility only) Check proximity to ports |
normalize_input | true / false | Enable AI / geocoding normalisation of the input. Set true for CSV/XLSX (which need geocoding), false for ready GeoJSON. |
production_volume | e.g. 1000 | (Plot only) Claimed total annual production (tonnes). Used for circumvention detection — compares declared volume against expected yield for the total plot area. |
async_mode | true / false | If true, dispatches the heavy checks asynchronously and returns a task id to poll via /validate_locations/{task_id}/status. |
limit | 100 | Max ranked features to return (top N by confidence). Default 100. |
include_deforestation and include_article10_check are not the current contract. Deforestation is rolled into include_art9; Article 10 procedural risk is include_art10; adverse-media/legality is include_legality.
POST /validate_locations?location_type=plot&include_art9=true&include_art10=true&include_eudr_profile=true&commodity_type=palm_oil&limit=10000
Content-Type: multipart/form-data
Body: file=@plots.geojson
POST /validate_locations?location_type=facility&commodity_type=timber&include_art9=true&include_legality=true&include_eudr_profile=true&check_road_access=true&normalize_input=true
Content-Type: multipart/form-data
Body: file=@facilities.csv
Returns a GeoJSON FeatureCollection with collection-level properties containing validation results:
| Field | Type | Description |
|---|---|---|
overall.risk | string | Overall risk level (high / medium / low) |
total_plots | int | Number of plots screened |
total_plot_area_ha | float | Total area in hectares |
plot_validity.shape_validity | object | risk, issues (count by type: self-intersections, slivers, etc.) |
plot_validity.commodity_presence | object | risk, commodity_absence_count (plots not overlapping declared commodity) |
plot_validity.protected_area_overlap | object | risk, protected_area_overlap_count (plots overlapping WDPA areas) |
deforestation | object | risk, plots_with_deforestation, deforestation_area_ha, plot_deforestation_pct |
circumvention | object | risk, deviation_pct (spatial deviation from expected patterns) |
cherry_picking | object | risk, deforestation_pct (comparison of inside-vs-outside deforestation rates) |
overall.investigate_categories | object | Map of category name to plot count needing investigation |
Each feature in the collection includes per-plot scores for shape validity, commodity, protected areas, and deforestation.
Returns a GeoJSON FeatureCollection with per-facility confidence scoring. Each feature includes:
| Field | Type | Description |
|---|---|---|
confidence_score | float | Per-facility confidence score (0-1). Penalty-based: starts at 1.0 and deducts for missing attributes. See Scoring. |
confidence_level | string | high (≥0.7) / medium (≥0.45) / low |
commodity_type | string | Detected or declared commodity type |
commodity_presence | float | Commodity overlap score (0-1) |
poi_presence | int | 1 if facility is near known POIs (mills, factories), 0 otherwise |
building_presence | int | 1 if building footprints detected in 100m buffer, 0 otherwise |
high_density | int | 1 if building density > 50% of buffer area (urban), 0 otherwise. Negative signal — penalizes score. |
road_access | int | 1 if road access within 500m (default: checked) |
water_access | int | 1 if waterway access within 1km (optional) |
port_proximity | int | 1 if port within 50km (optional) |
producer_name | string | Extracted producer/company name from input columns (if available) |
producer_confirmed | bool | Whether producer confirmation score exceeds threshold |
producer_confirmation_score | float | Producer confirmation score (0-1) |
Collection-level properties include aggregated counts:
| Field | Type | Description |
|---|---|---|
total_facilities | int | Number of facilities screened |
confidence_score | float | Average confidence score (0-1) |
confidence_level | object | Distribution: { "high": N, "medium": N, "low": N } |
commodity_presence | int | Facilities near declared commodity |
poi_presence | int | Facilities near points of interest |
high_density | int | Facilities in high building-density areas |
road_access | int | Facilities with road access (if checked) |
water_access | int | Facilities with waterway access (if checked) |
port_proximity | int | Facilities near ports (if checked) |
When include_art10 and/or include_legality are set, two independent risk dimensions are returned. They are deliberately separate — do not conflate them.
| Field | Type | Description |
|---|---|---|
article10_risk_score | float (0–1) | Article 10(2) procedural risk from the canonical scorer (criteria a–j, including circumvention). Same rubric on plot and facility paths. |
article10_risk_level | string | low / medium / high / not_assessed |
legality_risk_score | float | Adverse-media (Article 9) risk, banded by source relevance: 0.20 (low) / 0.55 (medium) / 0.85 (high). On the facility path this is Article 9 only — the old 60/40 deterministic+Art9 blend was removed; read both fields and combine yourself if you need a single view. |
legality_risk_level / risk_level | string | low / medium / high, derived from the relevance tiers of the sources found. |
country_tier | string | EUDR Article 29 classification (low / standard / high / unknown). A routing gate that selects the due-diligence regime — it is not a weight in the risk scores. |
adverse_media_risk_score / adverse_media_risk_level alongside the legacy legality_risk_* fields. Both are populated; the legacy names are retained for backward compatibility.
Submit screened locations for full asynchronous processing. /batch_supply_shed is used for facility points (generates isochrone-based supply sheds), /batch_process is used for plot polygons (direct deforestation assessment).
| Parameter | Value | Purpose |
|---|---|---|
file | GeoJSON | Selected features from screening results |
collection_name | string | Collection name (auto-suggested: {commodity}_{country}_{date}) |
crop_type | e.g. "timber" | Commodity type for the batch |
stats | "eudr_deforestation" | Statistics to compute |
validate_locations | "false" | Skip validation (already validated during screening) |
force_reprocess | "false" | Whether to force reprocessing of existing collections |
eudr_dds | "true" | (batch_supply_shed only) Generate DDS audit trail |
POST /batch_supply_shed
Content-Type: multipart/form-data
Body:
file=@timber_Suriname_2026-03-11.geojson
collection_name=timber_Suriname_2026-03-11
crop_type=timber
stats=eudr_deforestation
validate_locations=false
force_reprocess=false
eudr_dds=true
Returns a JSON object with the batch job status and collection ID. Processing runs asynchronously via Prefect flows.
Downloads the EUDR audit trail as a ZIP archive containing the full Due Diligence Statement with deforestation results, satellite references, and compliance determination per plot.
GET /export_eudr_dds/?collections=0x49a7...
Response: Binary ZIP file. Trigger as a browser download.
locations_confidence = high/medium/low). Disable the download button for "Not Assessable" rows.
Endpoint: POST /validate_locations
The default view after login. Upload GeoJSON, CSV, or XLSX files to screen locations. The tab supports two location types:
| Option | Maps to | Default | Description |
|---|---|---|---|
| Location Type | location_type | plot | Plots (polygons) or Facilities (points/addresses) |
| Commodity Type | commodity_type | Auto-detect | cocoa, coffee, palm, rubber, soy, timber, cattle |
| Include deforestation check | include_art9 | unchecked | Run the Article 9 deforestation / commodity overlay |
| Include Article 10 risk check | include_art10 | unchecked | Run the Article 10(2) procedural-risk assessment |
| Include legality / adverse-media | include_legality | unchecked | Run the adverse-media legality check |
| Production volume | production_volume | — | (Plot only) For circumvention detection (t/yr) |
| Check road access | check_road_access | unchecked | (Facility only) Check road network proximity |
| Check water access | check_water_access | unchecked | (Facility only) Check waterway proximity |
| Check port proximity | check_port_proximity | unchecked | (Facility only) Check port proximity |
After screening, the sidebar shows summary cards and the map displays screened locations. A data table below the map shows all facilities/plots with sortable columns.
Facility table columns:
| Column | Source |
|---|---|
| Facility ID | facility_id or id or name, fallback: 1-based index |
| Address | facility_address or address, fallback: facility_name |
| Commodity | commodity_type |
| Country | country |
| Score | confidence_score (numeric, 0-1) |
| Level | confidence_level (high/medium/low) |
| Comm. Presence | commodity_presence |
| POI | poi_presence |
| Building | building_presence |
| Road | road_access |
| Water | water_access |
| Port | port_proximity |
| High Density | high_density |
| Producer Name | producer_name |
| Confirmed | producer_confirmed |
| Warning | primary_warning |
After screening, select facilities/plots via checkboxes and submit to batch processing. The collection name auto-suggests as {commodity}_{country}_{date} and updates on re-screening. Facilities go to /batch_supply_shed, plots go to /batch_process.
Endpoint: GET /fetch_supply_shed?stat_type=all&table_extension=eudr&limit=100000
Loads all producer assessments and displays them in a sortable table with risk-tiered compliance status.
Two sub-views:
company_name. Each group shows its worst-case status and total NC plots. Expandable/collapsible.Columns:
| Column | Source |
|---|---|
| Producer | company_name |
| Commodity | commodity |
| Admin 1 | admin1 |
| Country | country or country_code |
| Assessment | Derived: "Supply Shed" or "Direct Plots" (see risk logic) |
| Status | Derived: High Risk / Medium Risk / Compliant / Not Assessable |
| NC Plots | noncompliance_plot_count |
| NC % | noncompliance_area_perc * 100 (display as percentage) |
| Article 10 | legality_risk |
| Date | ingestion_date |
| Audit | "Download DDS" button |
Summary cards: Total Producers, Compliant, High Risk, Medium Risk (≤5%), Not Assessable.
Triggered when a user clicks a producer row. Fetches plot-level data via:
GET /fetch_deforestation_check?aggregate=false&limit=100000&filename={collection_id}
Shows individual plot polygons on the map with binary compliance status (Compliant / Non-Compliant based on >0.5 ha noncompliance area). Summary cards show Total Plots, Compliant, and Non-Compliant counts.
Facility confidence uses a penalty-based model: each facility starts at 1.0 and has points deducted for missing or negative attributes.
| Check | Condition | Penalty |
|---|---|---|
| Commodity Presence | commodity_presence == 0 | -0.50 |
| Commodity Presence (low) | 0 < commodity_presence < 1.0 | -0.25 |
| POI Presence | poi_presence == 0 | -0.20 |
| Building Presence | building_presence == 0 | -0.15 |
| Road Access | road_access == 0 | -0.15 |
| High Density (urban) | high_density == 1 | -0.10 |
water_access and port_proximity are informational only — they do not affect the confidence score.
| Level | Score Range | Color |
|---|---|---|
| High | ≥ 0.70 | Green (#16a34a) |
| Medium | ≥ 0.45 | Yellow (#ca8a04) |
| Low | < 0.45 | Red (#dc2626) |
A facility is classified as high_density (urban) when building footprint area exceeds 50% of the 100m buffer area around the facility point. This is a negative signal — commodity processing facilities are typically in rural or peri-urban areas, not dense urban centers.
On hover, the map tooltip shows the full scoring breakdown including each attribute's value and its penalty contribution. high_density = Yes should display in red (negative signal), while high_density = No displays in green (positive — rural area).
The API extracts producer_name from input CSV/XLSX columns by scanning for columns matching keywords: "name", "company", "producer", "facility". Columns that are part of the API's own output (facility_address, facility_id, commodity_type, scoring fields, etc.) are excluded from the scan to prevent false matches (e.g. address leaking into producer name).
Risk status is derived client-side from the API response fields. The key field is noncompliance_area_perc — the fraction of total assessed area flagged as non-compliant.
| Status | Condition | Color | Badge CSS Class |
|---|---|---|---|
| High Risk | noncompliance_area_perc > 0.05 (more than 5% of area) OR noncompliance_plot_count > 100 | Red (#dc2626) | high-risk |
| Medium Risk | noncompliance_area_perc > 0 or noncompliance_plot_count > 0 (any non-compliance, but below High Risk thresholds) | Yellow (#ca8a04) | medium-risk |
| Compliant | No non-compliance detected | Green (#16a34a) | compliant |
| Not Assessable | locations_confidence not in (high, medium, low) and no noncompliance data | Gray (#475569) | unknown |
function getComplianceStatus(p) {
const canDo = isAssessable(p.locations_confidence)
|| p.noncompliance_plot_count != null
|| p.noncompliance_area_ha != null;
if (!canDo) return { label: 'Not Assessable', cls: 'unknown' };
const ncPerc = Number(p.noncompliance_area_perc || 0);
const ncPlots = Number(p.noncompliance_plot_count || 0);
const ncArea = Number(p.noncompliance_area_ha || 0);
if (ncPerc > 0.05 || ncPlots > 100) return { label: 'High Risk', cls: 'high-risk' };
if (ncPerc > 0 || ncPlots > 0 || ncArea > 0)
return { label: 'Medium Risk', cls: 'medium-risk' };
return { label: 'Compliant', cls: 'compliant' };
}
Plot-level compliance uses a simple binary classification based on the noncompliance_area field (deforested area in hectares). A plot is non-compliant if more than 0.5 ha of deforestation is detected.
| Status | Condition | Color |
|---|---|---|
| Non-Compliant | noncompliance_area > 0.5 (ha) | Red (#dc2626) |
| Compliant | noncompliance_area ≤ 0.5 (ha) | Green (#16a34a) |
// Binary compliance for individual plots
const ncArea = Number(p.noncompliance_area || 0);
const isNonCompliant = ncArea > 0.5;
const status = isNonCompliant ? 'Non-Compliant' : 'Compliant';
/fetch_supply_shed). The plot detail view from /fetch_deforestation_check uses binary compliant/non-compliant.
if (area_ha_supply_shed > 0 && stat_type.includes('supply_shed')):
type = "Supply Shed" // isochrone-based area assessment
else:
type = "Direct Plots" // specific plot polygons provided
function isAssessable(confidence) {
return ['high', 'medium', 'low'].includes(confidence?.toLowerCase());
}
// A producer can be assessed if confidence is valid OR noncompliance data exists
function canAssess(p) {
return isAssessable(p.locations_confidence)
|| p.noncompliance_plot_count != null
|| p.noncompliance_area_ha != null;
}
The POC uses deck.gl with MapLibre GL. Any map library works.
Facility points colored by risk status. Sort features so higher-risk points render on top (last in the array):
const riskOrder = { 'compliant': 0, 'unknown': 1, 'medium-risk': 2, 'high-risk': 3 };
pointFeatures.sort((a, b) =>
(riskOrder[getComplianceStatus(a.properties).cls] || 0) -
(riskOrder[getComplianceStatus(b.properties).cls] || 0)
);
Fill colors:
[220, 53, 69, 200][202, 138, 4, 200][148, 163, 184, 200][22, 163, 74, 200]Point coordinates come from the feature's GeoJSON geometry, or parse facility_geo from metadata (WKT point) for facilities without geometry.
Facility points colored by confidence level:
Plot polygons colored by per-plot risk score or filtered by metric (overall risk, deforestation, protected area overlap).
Plot polygons from /fetch_deforestation_check, colored by binary compliance (>0.5 ha noncompliance area):
[220, 53, 69] (red)[22, 163, 74] (green)Overlay the supply shed boundary (blue outline) from the parent feature's geometry, and mark the facility location (yellow dot) if available.
The screening-to-processing workflow in the dashboard:
/validate_locations to get confidence scores and validation results./batch_supply_shed or /batch_process respectively.The collection name auto-suggests as {commodity}_{country}_{date} (e.g. timber_Suriname_2026-03-11) based on the first feature's properties. It updates automatically on re-screening.
stats=eudr_deforestation — Always set for EUDR enforcement workflowsvalidate_locations=false — Already validated during screening stepforce_reprocess=false — Prevents duplicate processing of existing collectionseudr_dds=true — (batch_supply_shed only) Generates the DDS audit trailEndpoint: GET /export_eudr_dds/?collections={collection_id}
Provide a "Download DDS" button for each assessed producer. Trigger a binary ZIP download. Disable the button for "Not Assessable" rows.
The ZIP contains the full Due Diligence Statement including:
Both /fetch_supply_shed and /fetch_deforestation_check return Newline-Delimited JSON (NDJSON). Each line is a complete JSON object (GeoJSON Feature).
async function fetchNDJSON(url, token) {
const resp = await fetch(url, {
headers: { 'Authorization': `Bearer ${token}` }
});
const features = [];
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // keep incomplete line in buffer
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
const obj = JSON.parse(trimmed);
if (obj.type === 'Feature') features.push(obj);
else if (obj.type === 'FeatureCollection') features.push(...(obj.features || []));
} catch (_) {}
}
}
// Process remaining buffer
if (buffer.trim()) {
try { const obj = JSON.parse(buffer.trim()); features.push(obj); } catch (_) {}
}
return features;
}
import requests, json
def fetch_ndjson(url, token):
resp = requests.get(url, headers={"Authorization": f"Bearer {token}"}, stream=True)
features = []
for line in resp.iter_lines(decode_unicode=True):
if line.strip():
features.append(json.loads(line))
return features
/refresh_token — store the Bearer token/validate_locations with location_type=facility or plot/batch_supply_shed (facilities) or /batch_process (plots)/fetch_supply_shed?stat_type=all&table_extension=eudr&limit=100000noncompliance_area_perc (see Section 6)/fetch_deforestation_check?aggregate=false&limit=100000&filename={collection_id}noncompliance_area > 0.5 ha, green otherwise/export_eudr_dds/?collections={collection_id}