{
  "openapi": "3.1.0",
  "info": {
    "title": "eudr.dev — EU Deforestation Regulation compliance API",
    "version": "0.3.0",
    "description": "eudr.dev is an independent AI agent that helps companies, SMEs, suppliers, buyers — and the AI agents that act on their behalf — navigate Reg. (EU) 2023/1115 via an A2A-native interface. Built on emem (https://github.com/Vortx-AI/emem), the open-source Earth-memory protocol that supplies every signed per-cell fact this agent cites. Generates signed Annex II Due Diligence Statements with content-addressed evidence and offline-verifiable receipts, not affiliated with, endorsed by, or sponsored by the European Commission or any EU institution. See /legal/disclaimer for the scope of service and /docs/audit/verify-offline for the offline-verification procedure.",
    "contact": {
      "name": "Vortx AI Private Limited",
      "email": "avijeet@vortx.ai"
    }
  },
  "servers": [
    {
      "url": "https://eudr.dev",
      "description": "This deployment"
    }
  ],
  "components": {
    "schemas": {
      "GeoJSONGeometry": {
        "type": "object",
        "description": "GeoJSON Geometry — Polygon, MultiPolygon, or Point. WGS84 (EPSG:4326). Coordinates ordered [lng, lat].",
        "required": [
          "type",
          "coordinates"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "Polygon",
              "MultiPolygon",
              "Point"
            ]
          },
          "coordinates": {}
        },
        "examples": [
          {
            "type": "Polygon",
            "coordinates": [
              [
                [
                  -6.11,
                  5.2
                ],
                [
                  -6.11,
                  5.205
                ],
                [
                  -6.105,
                  5.205
                ],
                [
                  -6.105,
                  5.2
                ],
                [
                  -6.11,
                  5.2
                ]
              ]
            ]
          }
        ]
      },
      "SpatialVerifyBody": {
        "type": "object",
        "required": [
          "commodity",
          "country"
        ],
        "description": "Supply either `polygon` (one plot) or `plots` (up to eight). Supplying neither is a 400 naming the omission.",
        "properties": {
          "polygon": {
            "$ref": "#/components/schemas/GeoJSONGeometry"
          },
          "plots": {
            "type": "array",
            "minItems": 1,
            "maxItems": 8,
            "description": "Several plots in one request, each keeping its own verdict row. Art. 4(1) taints the whole consignment if any single plot fails, so they belong in one call.",
            "items": {
              "type": "object",
              "required": [
                "geojson"
              ],
              "properties": {
                "plot_id": {
                  "type": "string",
                  "maxLength": 64,
                  "description": "Echoed on the matching verdict row. [A-Za-z0-9-_.:]"
                },
                "geojson": {
                  "$ref": "#/components/schemas/GeoJSONGeometry"
                }
              }
            }
          },
          "commodity": {
            "type": "string",
            "enum": [
              "cattle",
              "cocoa",
              "coffee",
              "oil_palm",
              "rubber",
              "soya",
              "wood"
            ]
          },
          "country": {
            "type": "string",
            "description": "ISO-3166-1 alpha-2.",
            "minLength": 2,
            "maxLength": 2
          },
          "quantity_kg": {
            "type": "number",
            "nullable": true
          },
          "hs_code": {
            "type": "string",
            "nullable": true
          },
          "evidence_detail": {
            "type": "string",
            "enum": [
              "cids",
              "anchor"
            ],
            "default": "cids",
            "description": "`cids` embeds every per-cell fact CID; `anchor` returns only the address and count, resolvable at /v1/evidence/{evidence_cid}. The verdict is identical either way. On a real plot the array is the large majority of the body."
          }
        }
      },
      "SpatialVerifyResponse": {
        "type": "object",
        "description": "The verdict, what was inspected to reach it, and what was NOT checked. Every response names the unresolved checks so a clean deforestation result is never read as a clean due-diligence file.",
        "properties": {
          "deforestation_free": {
            "type": "boolean"
          },
          "post_cutoff_loss_cells": {
            "type": "integer",
            "description": "Signed population tally, not the shipped sample."
          },
          "total_cells": {
            "type": "integer"
          },
          "verdict": {
            "type": "string",
            "enum": [
              "negligible",
              "non_negligible",
              "not_in_scope",
              "indeterminate"
            ],
            "description": "`not_in_scope` means the commodity is outside Annex I."
          },
          "risk_score": {
            "type": "number",
            "nullable": true
          },
          "country_tier": {
            "type": "string",
            "enum": [
              "low",
              "standard",
              "high"
            ]
          },
          "cut_off_date": {
            "type": "string",
            "example": "2020-12-31"
          },
          "per_plot": {
            "type": "array",
            "description": "One row per submitted plot, in submission order. A single-plot request still gets one row.",
            "items": {
              "type": "object",
              "properties": {
                "plot_id": {
                  "type": "string"
                },
                "verdict": {
                  "type": "string"
                },
                "cells_evaluated": {
                  "type": "integer"
                },
                "cells_with_post_cutoff_loss": {
                  "type": "integer"
                },
                "area_ha_approx": {
                  "type": "number",
                  "nullable": true
                }
              }
            }
          },
          "coverage": {
            "type": "object",
            "description": "What was actually inspected. A verdict over 2 % of a polygon and a verdict over all of it are different claims.",
            "properties": {
              "cells_evaluated": {
                "type": "integer"
              },
              "cell_size_m": {
                "type": "number",
                "example": 9.55
              },
              "sampled_area_ha": {
                "type": "number",
                "nullable": true
              },
              "plot_area_ha": {
                "type": "number",
                "nullable": true
              },
              "sampled_polygon_fraction": {
                "type": "number",
                "nullable": true,
                "description": "1.0 is a full sweep."
              },
              "sampling_note": {
                "type": "string",
                "nullable": true,
                "description": "The upstream responder's own disclosure, verbatim."
              }
            }
          },
          "loss_years_observed": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "Distinct Hansen loss years in the shipped sample, ascending. Dates what was found; never counts it."
          },
          "unresolved_checks": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Checks this engine does not perform. Art. 3(b) legality is documentary and unobserved here."
          },
          "evidence": {
            "type": "object",
            "description": "Note the nesting: `evidence_cid` lives HERE, not at the top level. An earlier revision of this document put it at the top level, which is a field the response has never carried.",
            "properties": {
              "evidence_cid": {
                "type": "string",
                "nullable": true,
                "description": "Content address of THIS response's fact list. BLAKE3 over its canonical JSON, so a caller can re-derive it from the returned CIDs. It is not a fingerprint of the question: emem signs each fact with the moment it was signed, so re-running the same check returns the same measurements under new addresses. Resolve at /v1/evidence/{evidence_cid}."
              },
              "fact_count": {
                "type": "integer",
                "description": "How many facts the check resolved. Always present."
              },
              "fact_cids": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Omitted entirely when `evidence_detail: \"anchor\"` was sent."
              },
              "responder_pubkey_b32": {
                "type": "string",
                "nullable": true
              },
              "algorithm": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "engine": {
            "type": "string"
          }
        }
      },
      "ApiError": {
        "type": "object",
        "description": "Every rejection on the /v1/* surface. A malformed body returns this with HTTP 400 and `error: \"invalid_request\"` — not a plain-text 422.",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Stable code to branch on, so a caller never string-matches prose that is free to be reworded.",
            "enum": [
              "invalid_request",
              "invalid_json",
              "invalid_content_type",
              "upstream_unavailable",
              "rate_limit_exceeded",
              "evidence_not_held",
              "task_not_found",
              "not_compiled",
              "erased",
              "error"
            ]
          },
          "message": {
            "type": "string",
            "description": "A sentence a human can read."
          },
          "field": {
            "type": "string",
            "nullable": true,
            "description": "The field at fault, where one is known."
          }
        }
      },
      "DDSGenerateBody": {
        "type": "object",
        "required": [
          "operator",
          "supplier",
          "product",
          "production"
        ],
        "properties": {
          "operator": {
            "type": "object",
            "description": "Operator block per Annex II (Art. 9(1)(a))."
          },
          "supplier": {
            "type": "object",
            "description": "Supplier identity + country of production."
          },
          "product": {
            "type": "object",
            "description": "HS code + description + quantity_kg."
          },
          "production": {
            "type": "object",
            "description": "Country of production + harvest window + geolocation."
          }
        }
      },
      "SignedReceipt": {
        "type": "object",
        "description": "ed25519-signed Annex II envelope. Verifiable offline — see /docs/audit/verify-offline.",
        "required": [
          "payload",
          "cid",
          "signature_b64",
          "pubkey_b32"
        ],
        "properties": {
          "payload": {
            "type": "object",
            "description": "Canonical Annex II JSON."
          },
          "cid": {
            "type": "string",
            "description": "CIDv1 of the canonical payload (BLAKE3 + multibase 'b' base32)."
          },
          "signature_b64": {
            "type": "string",
            "description": "ed25519 signature over the CID bytes, base64-encoded."
          },
          "pubkey_b32": {
            "type": "string"
          },
          "did": {
            "type": "string"
          },
          "issued_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    }
  },
  "paths": {
    "/v1/spatial/verify": {
      "post": {
        "summary": "Plot polygon → deforestation verdict + evidence CID.",
        "description": "Hansen GFC + ESA WorldCover + JRC GFC2020 via emem.dev. Returns the verdict against the 2020-12-31 cut-off in Art. 2(13) Reg. (EU) 2023/1115.",
        "operationId": "spatialVerify",
        "tags": [
          "agent"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SpatialVerifyBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verdict + evidence",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SpatialVerifyResponse"
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit (60 rpm sliding window per IP)"
          }
        }
      }
    },
    "/v1/evidence/{evidence_cid}": {
      "get": {
        "summary": "Page the fact set behind a check.",
        "description": "Resolve an `evidence_cid` minted by /v1/spatial/verify. The address is BLAKE3 over the canonical CID list, so it is re-derivable from the CIDs a response returned. A 404 means the handle is not held by this process (expired, or a restart); re-running the check returns the same measurements under new addresses rather than recovering these bytes, because each fact is signed with its signing time. For a citation that must outlive a day, cite the statement receipt.",
        "operationId": "resolveEvidence",
        "tags": [
          "agent"
        ],
        "parameters": [
          {
            "name": "evidence_cid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of content-addressed facts."
          },
          "404": {
            "description": "No evidence set is held for this address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/trace/{task_id}": {
      "get": {
        "summary": "The complete derivation chain for a compiled statement.",
        "description": "Everything here is already inside the signed statement; what this adds is order and attribution. Steps run from the polygon through grid addressing, signed measurement, algorithm evaluation against pinned registries, aggregation, the consignment verdict, citations, the independent guard verdict, the engine signature and the transparency-log anchor. Three keys sign parts of the chain and they are not interchangeable — `signing_parties` keeps them separate. A stage whose evidence is absent is emitted as absent WITH a reason rather than omitted, and `not_established` states what the chain does not show.",
        "operationId": "executionTrace",
        "tags": [
          "agent"
        ],
        "parameters": [
          {
            "name": "task_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The derivation chain (schema eudr.execution_trace.v1)."
          },
          "404": {
            "description": "No compile is held under that task_id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "The compile has not produced a statement yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "410": {
            "description": "The statement was erased under GDPR Art. 17.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "summary": "Model Context Protocol endpoint (JSON-RPC, not REST).",
        "description": "Dual-era Streamable HTTP MCP. Answers the legacy `initialize` handshake and the stateless 2026-07-28 revision (`server/discover`, per-request `_meta` protocol version, `resultType`, `ttlMs`/`cacheScope`, `Mcp-Method`/`Mcp-Name` validated against the body). This is NOT a REST resource and is described here only so a reader of this document knows it exists; the tool catalog is served by `tools/list`, and /.well-known/mcp.json carries the connection details. GET returns 405.",
        "operationId": "mcp",
        "tags": [
          "agent"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "A single JSON-RPC 2.0 request or notification. Batching is not supported."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response."
          },
          "202": {
            "description": "Notification accepted, no body."
          },
          "400": {
            "description": "Header/body mismatch (-32020) or unsupported protocol version (-32022)."
          },
          "404": {
            "description": "Unknown method (-32601)."
          }
        }
      }
    },
    "/v1/dds/generate": {
      "post": {
        "summary": "Operator + supplier + product + geolocation → signed Annex II Due Diligence Statement.",
        "description": "Runs the 7-stage compile pipeline: geo → forest → legality → tenure → risk → dds → settle. Returns a SignedReceipt envelope (ed25519, CIDv1).",
        "operationId": "ddsGenerate",
        "tags": [
          "agent"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DDSGenerateBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Task started; poll /a2a/tasks/{id} until state=completed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "402": {
            "description": "Payment required (paid mode only — currently off in free-preview)."
          },
          "429": {
            "description": "Rate limit"
          }
        }
      }
    },
    "/v1/receipt/verify": {
      "post": {
        "summary": "Signed receipt JSON → boolean + re-derived CID + signer pubkey.",
        "operationId": "receiptVerify",
        "tags": [
          "agent"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SignedReceipt"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verification result."
          }
        }
      }
    },
    "/v1/reference/countries": {
      "get": {
        "summary": "Country risk tiers per Implementing Reg. (EU) 2025/1093.",
        "operationId": "referenceCountries",
        "tags": [
          "agent",
          "reference"
        ],
        "responses": {
          "200": {
            "description": "Country table. Cacheable (Cache-Control: public, max-age=86400, immutable) with strong BLAKE3 ETag."
          }
        }
      }
    },
    "/v1/reference/commodities": {
      "get": {
        "summary": "Annex I commodity → HS prefix mapping per Reg. (EU) 2023/1115.",
        "operationId": "referenceCommodities",
        "tags": [
          "agent",
          "reference"
        ],
        "responses": {
          "200": {
            "description": "Commodity table. Cacheable."
          }
        }
      }
    },
    "/v1/tools.json": {
      "get": {
        "summary": "Single-source-of-truth tool catalog. Mirrors the MCP server's tool list and the agent card's skill list.",
        "operationId": "toolsJson",
        "tags": [
          "discovery"
        ],
        "responses": {
          "200": {
            "description": "Tool catalog."
          }
        }
      }
    },
    "/api/compile": {
      "post": {
        "summary": "Engineer-facing alias of /v1/dds/generate. Body shape: { scenario: <Scenario> }.",
        "operationId": "compile",
        "tags": [
          "engineer"
        ],
        "responses": {
          "200": {
            "description": "Task envelope."
          }
        }
      }
    },
    "/api/skills": {
      "get": {
        "summary": "Skill catalog with prices.",
        "operationId": "skills",
        "tags": [
          "discovery"
        ],
        "responses": {
          "200": {
            "description": "Skills list. In free-preview every priced skill returns price_eur=0."
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "summary": "Liveness probe.",
        "operationId": "health",
        "tags": [
          "ops"
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        }
      }
    },
    "/a2a/rpc": {
      "post": {
        "summary": "A2A v1.0 JSON-RPC entrypoint. Methods: SendMessage, GetTask, ListTasks.",
        "operationId": "a2aRpc",
        "tags": [
          "a2a"
        ],
        "responses": {
          "200": {
            "description": "JSON-RPC envelope."
          }
        }
      }
    },
    "/a2a/tasks/{id}": {
      "get": {
        "summary": "Fetch a task by id. The id is the bearer — 122 bits of UUIDv4 entropy.",
        "operationId": "getTask",
        "tags": [
          "a2a"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Task envelope (state, dds, signature)."
          },
          "404": {
            "description": "Not found / expired."
          }
        }
      },
      "delete": {
        "summary": "Erase a task (GDPR Art. 17 right to erasure).",
        "operationId": "eraseTask",
        "tags": [
          "a2a"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Erased."
          }
        }
      }
    },
    "/a2a/tasks/{id}/events": {
      "get": {
        "summary": "SSE stream of pipeline events (StageStart, StageDone, Completed, …).",
        "operationId": "taskEvents",
        "tags": [
          "a2a"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "text/event-stream",
            "content": {
              "text/event-stream": {}
            }
          }
        }
      }
    },
    "/.well-known/agent-card.json": {
      "get": {
        "summary": "A2A v1.0 agent card. Lists skills, ed25519 pubkey, JSON-RPC endpoint.",
        "operationId": "agentCard",
        "tags": [
          "discovery"
        ],
        "responses": {
          "200": {
            "description": "Agent card JSON."
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "agent",
      "description": "Stateless, semantic /v1/* surface for AI agents."
    },
    {
      "name": "engineer",
      "description": "Engineer-facing /api/* aliases."
    },
    {
      "name": "a2a",
      "description": "A2A v1.0 JSON-RPC + task FSM."
    },
    {
      "name": "discovery",
      "description": "Agent-card, OpenAPI, MCP, ai-plugin, tools catalog."
    },
    {
      "name": "reference",
      "description": "Country tiers + commodity HS prefixes (cacheable)."
    },
    {
      "name": "ops",
      "description": "Liveness + readiness probes."
    }
  ],
  "externalDocs": {
    "url": "https://eudr.dev/docs",
    "description": "Theory, methodology, architecture, and offline-verification guide."
  }
}