# CSV / Webhook connector — row schema

One row per consignment. One consignment per Annex II Due Diligence
Statement. Header is required; columns may appear in any order; unknown
columns are ignored and surface as a row-level warning.

Sample template: [`csv-sample.csv`](./csv-sample.csv) (downloadable at
`GET /app/api/orgs/:org_id/connectors/:connector_id/sample-csv`).

| Column | Type | Required | Annex II point | Note |
|---|---|---|---|---|
| `operator_name` | string | yes | 1 (Art. 9(1)(a)) | Operator placing the product on the EU market. |
| `operator_country` | ISO 3166-1 alpha-2 | yes | 1 | Country of establishment of the operator. |
| `operator_eori` | string | no | 1 | EORI per Reg. (EU) 952/2013 Art. 9. Required when crossing the EU customs border. |
| `operator_address` | string | no | 1 | Operator postal address. Never written to logs (GDPR Art. 5(1)(c)). |
| `supplier_name` | string | yes | 5 (Art. 9(1)(e)) | Trade name of the direct supplier. |
| `supplier_id` | string | yes | 5 | Supplier identifier (cooperative id, VAT number, internal code). |
| `supplier_country` | ISO 3166-1 alpha-2 | yes | 5 | Supplier country. |
| `product_hs_code` | string (with or without dots) | yes | 2 (Art. 9(1)(b)) | HS / CN code for the relevant product. |
| `product_description` | string | yes | 2 | Trade description, lot reference, fermentation state, etc. |
| `quantity_kg` | positive number | yes | 2 | Net mass in kilograms. Must be > 0. |
| `production_country` | ISO 3166-1 alpha-2 | yes | 3 (Art. 9(1)(c)) | Country of production. |
| `harvest_window` | string | yes | 4 (Art. 9(1)(c)) | Free text or `YYYY-MM-DD/YYYY-MM-DD` interval. |
| `plot_geojson` | JSON-encoded GeoJSON Polygon or Point | yes | 3 (Art. 9(1)(d)) | Plot geometry. Polygons for plots > 4 ha; ≥ 6-decimal point allowed for smaller plots (Art. 2(28)). |
| `producer_name` | string | no | 3 (auditor expectation) | Primary producer / farmer behind the supplier. |
| `producer_kind` | string | no | 3 | One of `cooperative`, `smallholder`, `plantation`, `concession`, `family_farm`. |
| `customs_declaration_ref` | string | no | — (Art. 4(2)) | MRN or similar reference linking the DDS to the customs file. |

## Validation behaviour

- The header row is parsed first. If none of its column names match any
  required or optional column, the upload is rejected with `400` — the
  caller has almost certainly omitted the header line.
- Missing required columns reject the whole upload with `400`.
- Per-row validation failures (bad GeoJSON, unparseable quantity, missing
  required value) are accumulated as warnings; the row is skipped and the
  upload continues. The response carries the warnings array.
- An upload with zero successfully parsed rows returns `400` with the
  warnings in the body. Otherwise the response is `200`.
- Unknown columns are ignored and reported once (not per row).
- ISO-3166-1 alpha-2 shape (two ASCII letters) is checked superficially;
  the country-benchmark table catches unknown codes downstream.

## Lineage

Each produced Scenario carries a `from_connector` block:

```json
{
  "connector_id": "<uuid of the connector row>",
  "kind": "csv_webhook",
  "source_ref": "row-<N>"
}
```

where `N` is the spreadsheet-gutter line number (header = row 1, first
data row = row 2). The receipt records this provenance so an auditor can
trace any DDS back to the originating CSV row.
