ForestTrace EUDR Compliance Checker avatar
ForestTrace EUDR Compliance Checker

Pricing

$0.05 / actor start

Go to Apify Store
ForestTrace EUDR Compliance Checker

ForestTrace EUDR Compliance Checker

Batch-check suppliers, products, and locations for EUDR compliance. This Actor runs automated EUDR checks via ForestTrace APIs and generates structured results and compliance summaries for audits, reporting, and automation workflows.

Pricing

$0.05 / actor start

Rating

0.0

(0)

Developer

Hayder Al-Khalissi

Hayder Al-Khalissi

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

8 days ago

Last modified

Share

ForestTrace EUDR API & Apify Actor

A production-ready API for EUDR (EU Deforestation Regulation) compliance validation, due diligence preparation, risk scoring, and reporting. This repo also provides an Apify Actor that runs many API jobs with concurrency, retries, and structured output.


Apify Actor

The Actor runs on Apify with no external API required. You can either:

  • In-Actor mode (default): Omit backend in the input. Jobs run entirely inside the Actor (geo validate, due-diligence prepare, risk score, report generate). No external API calls.
  • External API mode: Set backend.baseUrl to your own HTTPS API to call it for each job instead.

Input: jobs (required), and optionally backend, execution, output, webhook. Results go to the Dataset and a summary + report to the default Key-Value store.

Input

FieldDescription
backendOptional. Leave empty to run in-Actor (no external API). Or baseUrl (https), optional auth.
jobsArray of { id?, endpoint, method, body?, query? } (max 10,000). Endpoints: /v1/geo/validate, /v1/due-diligence/prepare, /v1/risk/score, /v1/reports/generate
executionOptional concurrency (default 5), maxRetries (default 3)
outputOptional writeDataset (default true), writeSummary (default true)
webhookOptional url (https), method (POST/GET) to notify at end

Output

  • Dataset: One item per job with { index, jobId, endpoint, ok, status, data, durationMs, error? }.
  • Key-Value store: summary.json (counts, duration, by-endpoint stats), report.md (markdown report), OUTPUT (same as summary).

Security

  • Secrets: Never logged. Prefer Actor environment variables for auth (e.g. WORKER_TOKEN). Do not put secrets in input when possible.
  • Headers: Only the single allowed auth header from config is sent; no arbitrary headers from user input.
  • baseUrl: Must be https.
  • Job body: Max 1MB per job.

Run locally

npm install
npm start
# or: apify run

No API keys needed for in-Actor mode: omit backend in input and jobs run inside the Actor. Input is read from Apify (e.g. default key-value store or apify run with stored input).

Dependencies

  • apify – Actor SDK
  • p-limit – Concurrency
  • tsx – Run TypeScript

Publishing to the Apify Store

  1. Push the latest build
    apify push (or use a connected Git repo so builds stay up to date).

  2. Open Publication
    In Apify Console go to your Actor → Publication tab.

  3. Display information
    Under Publication > Display information fill in:

    • Icon/logo – Upload a square image (e.g. 120×120 px). Avoid copyrighted or branded art.
    • Actor name – e.g. "ForestTrace EUDR Actor" (short, ~40–50 chars for SEO).
    • Description – One clear paragraph; 140–156 characters is ideal for search.
    • Categories – Pick relevant ones (e.g. "Data extraction", "Compliance", "Automation").
    • SEO title & description – Optional but recommended for Store search.
  4. README
    The Store uses the Actor’s README. Yours is in the repo; it’s synced when you push. You can also edit the README in the Console under the Actor’s Source tab if needed.

  5. Publish
    When all required fields are done, Publish to Store becomes available. Click it to make the Actor public.

  6. Verify
    Search for your Actor on apify.com/store and check its page.


API (standalone server)

To run the API server locally (not the Actor):

$npm run start:server

📚 Documentation

  • API_DOCUMENTATION.md - Complete API guide with examples

Features

  • Geo-validation with polygon validation and bbox calculation
  • Due diligence preparation with data normalization
  • Risk scoring with configurable rules
  • Report generation in JSON and CSV formats
  • Async job processing with in-memory storage
  • In-memory caching with SHA-256 content hashing (24-hour TTL)
  • Full CORS support
  • Request tracking with unique request IDs
  • Comprehensive error handling with validation details

Quick Start

Prerequisites

  • Node.js 18+ and npm

Installation

# Install dependencies
npm install

Configure environment (optional)

For optional proxy secret validation, set:

  • PROXY_SECRET_REQUIRED – set to true to require the proxy secret header
  • PROXY_SECRET – secret value for server-to-server auth

Copy .env.example to .env and edit, or set in your environment.

Run the API

# Start the standalone API server (default port 8787, or set PORT)
npm run start:server

The API will be available at http://localhost:8787. To run the Apify Actor instead (in-Actor or external API mode), use npm start or apify run.

API Endpoints

Base URL (local): http://localhost:8787

1. Health Check

GET /health

Check API health and version.

$curl http://localhost:8787/health

Response:

{
"ok": true,
"version": "1.0.0",
"request_id": "req_1234567890_abc123xyz"
}

2. Geo Validation

POST /v1/geo/validate

Validate GeoJSON geometry (Polygon or MultiPolygon). Returns bounding box and warnings for high vertex counts.

curl -X POST http://localhost:8787/v1/geo/validate \
-H "Content-Type: application/json" \
-d '{
"geometry": {
"type": "Polygon",
"coordinates": [[
[102.0, 0.0],
[103.0, 0.0],
[103.0, 1.0],
[102.0, 1.0],
[102.0, 0.0]
]]
}
}'

Response:

{
"request_id": "req_1234567890_abc123xyz",
"valid": true,
"bbox": [102.0, 0.0, 103.0, 1.0]
}

Error Example (invalid polygon):

{
"error": "Polygon ring must be closed (first coordinate must equal last)",
"request_id": "req_1234567890_abc123xyz"
}

3. Due Diligence Preparation

POST /v1/due-diligence/prepare

Normalize and prepare due diligence data. Returns normalized payload with input hash.

curl -X POST http://localhost:8787/v1/due-diligence/prepare \
-H "Content-Type: application/json" \
-d '{
"operator": {
"name": "Acme Forest Products",
"country": "us",
"contact": "contact@acme.com"
},
"product": {
"commodity": "TIMBER",
"quantity": 50000,
"unit": "KG",
"hs_code": "440710"
},
"supply_chain": {
"supplier_name": "Forest Supplier Ltd",
"supplier_country": "ca",
"origin_country": "us"
},
"geo": {
"geometry": {
"type": "Polygon",
"coordinates": [[
[102.0, 0.0],
[103.0, 0.0],
[103.0, 1.0],
[102.0, 1.0],
[102.0, 0.0]
]]
}
}
}'

Response:

{
"request_id": "req_1234567890_abc123xyz",
"prepared_at": "2024-01-15T10:30:00.000Z",
"input_hash": "sha256:a1b2c3d4...",
"normalized": {
"operator": {
"name": "Acme Forest Products",
"country": "US",
"contact": "contact@acme.com"
},
"product": {
"commodity": "timber",
"quantity": 50000,
"unit": "kg",
"hs_code": "440710"
},
"supply_chain": {
"supplier_name": "Forest Supplier Ltd",
"supplier_country": "CA",
"origin_country": "US"
},
"geo": {
"geometry": {
"type": "Polygon",
"coordinates": [[
[102.0, 0.0],
[103.0, 0.0],
[103.0, 1.0],
[102.0, 1.0],
[102.0, 0.0]
]]
}
}
}
}

Caching: Subsequent identical requests return cached data with X-Cache: HIT header.


4. Risk Scoring

POST /v1/risk/score

Calculate risk score based on normalized data. Returns score (0-1), level (low/medium/high), and flags.

curl -X POST http://localhost:8787/v1/risk/score \
-H "Content-Type: application/json" \
-d '{
"normalized": {
"operator": {
"name": "Acme Forest Products",
"country": "US"
},
"product": {
"commodity": "timber",
"quantity": 150000,
"unit": "kg"
},
"supply_chain": {
"supplier_name": "Forest Supplier Ltd"
},
"geo": {
"geometry": {
"type": "Polygon",
"coordinates": [[[102.0, 0.0], [103.0, 0.0], [103.0, 1.0], [102.0, 1.0], [102.0, 0.0]]]
}
}
}
}'

Response:

{
"request_id": "req_1234567890_abc123xyz",
"score": 0.5,
"level": "medium",
"flags": [
"missing_hs_code",
"high_quantity",
"missing_supplier_country"
]
}

Risk Calculation:

  • Base score: 0.1 per flag
  • +0.2 if quantity > 100,000
  • Level: low (<0.3), medium (<0.7), high (≥0.7)

5. Report Generation

POST /v1/reports/generate?format=json|csv

Generate compliance report in JSON or CSV format.

JSON Format

curl -X POST "http://localhost:8787/v1/reports/generate?format=json" \
-H "Content-Type: application/json" \
-d '{
"normalized": {
"operator": {
"name": "Acme Forest Products",
"country": "US",
"contact": "contact@acme.com"
},
"product": {
"commodity": "timber",
"quantity": 50000,
"unit": "kg",
"hs_code": "440710"
},
"supply_chain": {
"supplier_name": "Forest Supplier Ltd",
"supplier_country": "CA"
},
"geo": {
"geometry": {
"type": "Polygon",
"coordinates": [[[102.0, 0.0], [103.0, 0.0], [103.0, 1.0], [102.0, 1.0], [102.0, 0.0]]]
}
}
}
}'

Response:

{
"request_id": "req_1234567890_abc123xyz",
"format": "json",
"operator": {
"name": "Acme Forest Products",
"country": "US",
"contact": "contact@acme.com"
},
"product": {
"commodity": "timber",
"quantity": 50000,
"unit": "kg",
"hs_code": "440710"
},
"supply_chain": {
"supplier_name": "Forest Supplier Ltd",
"supplier_country": "CA",
"origin_country": "N/A"
},
"geo_summary": {
"geometry_type": "Polygon",
"has_coordinates": true
},
"risk_placeholder": {
"note": "Risk assessment can be obtained via /v1/risk/score endpoint"
}
}

CSV Format

curl -X POST "http://localhost:8787/v1/reports/generate?format=csv" \
-H "Content-Type: application/json" \
-d '{
"normalized": {
"operator": {
"name": "Acme Forest Products",
"country": "US"
},
"product": {
"commodity": "timber",
"quantity": 50000,
"unit": "kg"
},
"supply_chain": {
"supplier_name": "Forest Supplier Ltd",
"supplier_country": "CA"
},
"geo": {
"geometry": {
"type": "Polygon",
"coordinates": [[[102.0, 0.0], [103.0, 0.0], [103.0, 1.0], [102.0, 1.0], [102.0, 0.0]]]
}
}
}
}'

Response:

{
"request_id": "req_1234567890_abc123xyz",
"format": "csv",
"content": "field,value\noperator_name,Acme Forest Products\noperator_country,US\n..."
}

6. Async Job Submission

POST /v1/jobs/submit

Submit batch processing job. Returns immediately with job ID for status polling.

curl -X POST http://localhost:8787/v1/jobs/submit \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"operator": {"name": "Company A", "country": "US"},
"product": {"commodity": "timber", "quantity": 1000, "unit": "kg"},
"supply_chain": {"supplier_name": "Supplier A"},
"geo": {"geometry": {"type": "Polygon", "coordinates": [[[0,0],[1,0],[1,1],[0,1],[0,0]]]}}
},
{
"operator": {"name": "Company B", "country": "CA"},
"product": {"commodity": "wood", "quantity": 2000, "unit": "kg"},
"supply_chain": {"supplier_name": "Supplier B"},
"geo": {"geometry": {"type": "Polygon", "coordinates": [[[0,0],[1,0],[1,1],[0,1],[0,0]]]}}
}
]
}'

Response (202 Accepted):

{
"request_id": "req_1234567890_abc123xyz",
"job_id": "job_1234567890_xyz789abc",
"status": "queued"
}

7. Job Status Check

GET /v1/jobs/{job_id}

Check status of submitted job.

$curl http://localhost:8787/v1/jobs/job_1234567890_xyz789abc

Response (completed):

{
"job_id": "job_1234567890_xyz789abc",
"status": "done",
"created_at": "2024-01-15T10:30:00.000Z",
"completed_at": "2024-01-15T10:30:05.000Z",
"items_count": 2,
"result": {
"processed": 2,
"results": [
{
"item_index": 0,
"operator": "Company A",
"commodity": "timber",
"status": "processed"
},
{
"item_index": 1,
"operator": "Company B",
"commodity": "wood",
"status": "processed"
}
]
},
"request_id": "req_1234567890_def456ghi"
}

Response (not found):

{
"error": "Job not found: job_invalid_id",
"request_id": "req_1234567890_abc123xyz"
}

Optional proxy authentication

When PROXY_SECRET_REQUIRED is set to true, requests must include the X-Proxy-Secret header with the value from PROXY_SECRET.


Error Handling

Error Response Format

All errors follow a consistent format:

{
"error": "Error message",
"request_id": "req_1234567890_abc123xyz",
"details": [
{
"path": "field.name",
"message": "Field 'field.name' is required"
}
]
}

HTTP Status Codes

  • 200 - Success
  • 202 - Accepted (async job)
  • 400 - Bad Request (invalid JSON, invalid Content-Type)
  • 403 - Forbidden (invalid proxy secret)
  • 404 - Not Found (route or resource not found)
  • 422 - Unprocessable Entity (validation errors)
  • 500 - Internal Server Error

Validation Errors

Field validation errors include path and message:

curl -X POST http://localhost:8787/v1/due-diligence/prepare \
-H "Content-Type: application/json" \
-d '{}'

Response (422):

{
"error": "Validation failed",
"request_id": "req_1234567890_abc123xyz",
"details": [
{"path": "operator.name", "message": "Field 'operator.name' is required"},
{"path": "operator.country", "message": "Field 'operator.country' is required"},
{"path": "product.commodity", "message": "Field 'product.commodity' is required"}
]
}

Caching

Cache Strategy

  • Cache key format: v1:<route>:<input_hash>
  • Input hash: SHA-256 of canonical JSON (sorted keys)
  • TTL: 24 hours (86,400 seconds)
  • Storage: in-memory cache

Cache Headers

Responses include cache status:

  • X-Cache: HIT - Response served from cache
  • X-Cache: MISS - Response computed and cached

Cached Endpoints

The following endpoints use caching:

  • /v1/due-diligence/prepare
  • /v1/risk/score
  • /v1/reports/generate

Cache entries expire automatically after 24 hours. Restart the server to clear the in-memory cache.


Project Structure

foresttrace-eudr-api/
├── src/
│ └── index.ts # API server and handlers
├── package.json # Dependencies and scripts
├── tsconfig.json # TypeScript configuration
└── README.md # This file

Development

Type Checking

$npm run typecheck

Testing Locally

Start the server and test endpoints:

npm start
# In another terminal
curl http://localhost:8787/health

Environment Variables

Optional (for proxy validation):

  • PROXY_SECRET_REQUIRED – set to true to require proxy secret
  • PROXY_SECRET – secret for the X-Proxy-Secret header

Use a .env file (gitignored) or set in your environment.


Security Best Practices

  1. Never commit secrets – Keep .env out of version control
  2. Validate all inputs – Strict JSON schema validation
  3. Rate limiting – Implement per-IP rate limits if needed
  4. CORS configuration – Restrict origins in production if needed
  5. Audit logging – Log all requests with request IDs
  6. Rotate secrets – Regularly update proxy secret if used

Troubleshooting

Common Issues

Issue: 403 Forbidden: invalid proxy secret

  • Solution: Check PROXY_SECRET_REQUIRED and X-Proxy-Secret header

Issue: 400 Bad Request: Content-Type must be application/json

  • Solution: Add -H "Content-Type: application/json" to curl commands

Issue: Cache not working

  • Solution: Cache is in-memory; restart the server to clear it

License

MIT


Version History

  • 1.0.0 - Initial release
    • Geo validation
    • Due diligence preparation
    • Risk scoring
    • Report generation (JSON/CSV)
    • Async job processing
    • In-memory caching
    • Optional proxy secret auth