Verdict overrides

One endpoint set for every kind of operator finding on a DDS record. A finding is a written, attributed statement an operator makes about what Epoch computed — either to overturn it, or to justify it without changing it.

EndpointPurpose
Set Verdict OverrideCreate or replace the finding on one (axis, subject)
Remove Verdict OverrideDelete that finding
List Verdict OverridesRead every finding on the record's current version
Batch Verdict OverrideFlip every subject on one axis in one call

These replaced PUT/DELETE /:recordId/plots/:plotId/justification, which were removed in EPO-1983. That pair was the plot-compliance axis with no basis, no document and no validity window.

Migrating from the removed pair. It is not a path swap. The old PUT preserved an omitted compliance_override and an omitted evidence_urls; this one is a full replace, so an omitted field becomes null / []. Send every field you want to keep. The response changed too: the old endpoints returned an operation envelope with an action and a previous/next delta, while this one returns the finding row keyed axis:subject_type:subject_id. Transition history now lives in the mitigation event log.

Bulk PATCH /config still accepts set_justifications / remove_justifications and is unchanged.

The three axes

An axis is one dimension of the compliance decision. Each has its own grain, its own effect on submit, and its own accepted evidence.

AxisGrainsubject_id isEffect on submitAccepts documents of category
plot_complianceplota plot idhard — an override to compliant clears the plots_non_compliant blockersupporting_document, certificate
legalityrecordthe record idsoft — warns, never blockslegality_evidence, certificate
exclusionplota plot idhard — the plot leaves the filing entirelysupporting_document, certificate, due_diligence_statement

The exclusion axis removes the plot from the filing. Writing an exclusion finding is what takes a plot out of the DDS: it disappears from GET /plots, appears on GET /plots/excluded, drops out of every report.geojson?include= mode, and is never sent to TRACES. It is still listed in the audit-trail PDF with its attestation, so the filing records what was left out and why.

Deleting the finding is the only way back in. This replaced POST .../plots/:plotId/exclude and .../include, which were removed along with the table behind them — the same PUT that previously only annotated now performs the exclusion.

subject_type defaults to plot. For a legality finding you must send subject_type: "record" and the record's own id as subject_id.

Every subject is validated against something you own, and a mismatch is a 400:

  • a plot subject must be a plot in the record's current version;
  • a record subject must be this record — naming another one is refused, because the report looks findings up by the record's own id and would never render it;
  • entity is in the vocabulary but no axis resolves it, so it is refused outright rather than storing an id nothing can check.

Override versus attestation

Both are findings. The difference is one field.

  • Override — send verdict_override. The plot or record now reads as compliant / non_compliant whatever the satellite signal says.
  • Attestation — omit verdict_override (or send null). The computed verdict stands; you have only put a reason on the record.

This distinction is load-bearing. An exclusion finding never carries a verdict, because a plot outside the supply base is not being declared compliant — it is being declared out of scope. And a legality finding with no verdict_override leaves the computed legality verdict in force; the audit trail will not claim legality was cleared when nobody cleared it.

The computed signal always survives

An override never replaces what Epoch found. The report and the PDF carry both, side by side, so a reviewer can weigh the attestation against the evidence that triggered it. A plot flipped to compliant still shows its deforested hectares.

Validity windows

valid_from / valid_to bound how long a finding applies. Both are optional; omit them for a finding that does not expire.

The exclusion axis refuses them with a 400. An exclusion is a scope decision, not time-bounded evidence, so letting one expire would pull a plot back into a filing with nobody acting and no audit event. Delete the finding to re-include the plot.

This exists because FLEGT and SVLK certificates are issued per shipment. A clearance that was true in March must not silently carry into July. Once the clock passes valid_to the finding stops counting with no further action from anyone: the plot reverts to its computed verdict, drops out of ?include=compliant, and trips the submit gate again.

Expired findings are retired, not deleted. They stay in List Verdict Overrides and stay printed in the audit trail, marked expired — the evidence of what was attested, and when, remains part of the record.

Document-backed findings

Send document_attachment_id and the finding's provenance becomes document instead of manual; the audit trail then prints the document's filename beside the attestation. Upload the file through the Attachments endpoints first.

The attachment is checked three ways, and any failure is a 400:

  • it exists;
  • it belongs to your organization — you cannot cite another operator's document, which matters because the audit trail prints its filename;
  • it is not archived — a withdrawn document is not live evidence;
  • its category is one this axis accepts (table above).

Freeze on submit

Once a version is filed to TRACES, its findings are immutable. Every write on this folder answers 409 CONFLICT. Cut a new version to make further changes; findings carry forward automatically, with plot ids remapped if a refresh changed them.

Audit trail

Every write appends config.finding_set; every delete appends config.finding_removed. Both carry the reason you supplied as payload.note with payload.source = "user", the acting user and organization, and — for a plot-grain finding — the plot as target_plot_id. A rejected write appends nothing.

A batch write appends one config.finding_batch_set naming every subject it moved, whatever the plot count.

Required scope: eudr:write to set or remove, eudr:read to list.