Update DDS Record

Patch the name, operator, commodity, and/or the editable submission metadata (internal_reference_number, activity_type, country_of_activity, geo_location_confidential, comment) on an existing DDS record. These inputs live on the record's current version (latest version_number); only the fields the caller actually sends are written, absent fields keep their current value (RFC 7396 — JSON Merge Patch semantics).

The record's filter and scope (commodity / country_code), version data, and TRACES post-submission references are immutable and never touched here. The filing identity fields (operator_role, associated_statements) are also not editable here — they fix the legal entity the DDS is filed under and are stamped at record creation. No post-create writer exists for them today: the Create Authorisation endpoint records consent only (its former submission_metadata stamp was removed so a re-grant can never overwrite staged fields).

Internal reference number

internal_reference_number is the operator's own reference for the submission (TRACES V3: max 35 chars). It is optional — you may stage it on the draft here so it round-trips on reads (surfaced as the top-level internal_reference_number attribute), or leave it unset and the Submit DDS flow auto-generates a stable fallback at filing time. Send null to clear a staged value back to auto-generate-on-submit; absent leaves it unchanged.

Audit lock

Editing is gated on the current version not having any submit-lifecycle event in the audit log (dds_mitigation_event_log). Once version.submit_started, version.submitted, or version.marked_submitted has been appended for the current version, its operator / commodity inputs are part of the audit trail and the endpoint responds 409 CONFLICT. The gate is purely on submission lifecycle — the dashboard lifecycle_status (accepted / rejected / stale) and the audit-log computed_status.status are surface views over the same events.

Effectively: edit while pending, lock once filed (or mid-filing).

Path params

ParamDescription
recordIdDDS record id

Request body

All blocks and fields are optional. The example shown above patches operator.phone, the commodity of a rubber tyre import (commodity.hs_code 4011, commodity.product_quantity_kg, and the 40 NAR supplementary unit TRACES requires on 4011 for IMPORT), and the staged submission metadata; everything else stays as it was. A "set this back to default" semantics is not supported for required fields — once provided at create time, fields cannot be cleared. For nullable fields (name, internal_reference_number, country_of_activity, comment, commodity.species_common_name, commodity.species_scientific_name, commodity.supplementary_unit, commodity.supplementary_unit_qualifier) sending null clears the value, and absent leaves it.

Editable blocks at a glance

BlockWhat it representsEditable here?
operatorThe legal entity actually placing the product on the EU market (the EUDR-accountable party).Yes — partial. Same field shape as on create.
commodityThe goods this DDS covers (HS code, product type, description, product_quantity_kg, the goodsMeasure supplementary_unit / supplementary_unit_qualifier pair, species).Yes — partial. Same field shape as on create.
internal_reference_numberThe operator's own reference for the submission (top-level attribute).Yes — optional; null clears it (auto-generated at submit when unset).
activity_type / country_of_activity / geo_location_confidential / commentThe remaining statement-level TRACES inputs, flat on attributes — the same editable subset the Submit DDS one-call body accepts, so a draft can be fully staged before filing. Staged values read back on the draft's submission block.Yes — see the field rules below.
operator_role / associated_statementsThe filing identity (who the DDS is filed for).No — 400. Writable only atomically with a fresh consent grant via Create Authorisation.

See Create Audit Trail for the field-by-field reference, per-field validation rules, and the full "who/what each block represents" preamble. Validation applies to every field that is present: an invalid email or hs_code is still a 400 even if the rest of the body is valid.

TRACES V3 field rules enforced here

  • commodity.supplementary_unit / supplementary_unit_qualifier — the optional goodsMeasure second TARIC unit (net weight in kg always rides on product_quantity_kg). The qualifier is a UN/CEFACT Rec20 code: MTQ (cubic metres — timber volume), NAR (number of articles) or KSD (kilograms of dry substance — pulp). KGM is rejected (it is net weight's unit). The amount and qualifier must be present together: setting one and clearing the other — after merging with the stored values — is a 422.

  • commodity species — species_scientific_name / species_common_name are required only for timber / timber-derived commodities (HS chapters 44, 47); enforced at submit via the species_missing pre-submission blocker, not rejected here.

  • activity_type — DOMESTIC, IMPORT, or EXPORT. Cross-field rules run against the merged (stored + patch) state, so patching one half of an invariant is a 422: DOMESTIC requires the merged country_of_activity to be null; IMPORT/EXPORT require the merged operator to be EORI-identified (not VAT); a non-EU operator (outside EU-27 + XI) can only file IMPORT. Two ergonomic carve-outs: (a) the activity/operator rules fire only when the patch actually changes a participating value — re-sending a field at its stored value is a no-op, and a pre-existing (e.g. create-time default) combination is tolerated until the operator_identifier_invalid / activity_not_allowed_for_operator_country pre-submission blockers stop it at filing; (b) switching to DOMESTIC without sending country_of_activity auto-clears any stored country (the field is hidden on DOMESTIC forms, so requiring an explicit null would trap them) — the record_update audit event carries the cleared value as country_of_activity_autocleared. Explicitly sending DOMESTIC together with a non-null country_of_activity is still a 422.

    A 422 from the two activity/operator rules carries a data.details.issues array anchoring the rejection to the request-body fields that produced it, shaped like flattened Zod issues ({ "path": [...], "message": "..." }). Both participating fields are listed, because either one is a valid fix — an IMPORT with a VAT-identified operator is resolved by switching the identifier to EORI or by switching the activity to DOMESTIC, and the API cannot know which the caller intended:

    {
      "error": "For IMPORT the operator must be identified by EORI (not VAT); ...",
      "code": "VALIDATION_ERROR",
      "tracking_id": "req-1a2b3c",
      "details": {
        "issues": [
          { "path": ["data", "attributes", "activity_type"], "message": "..." },
          { "path": ["data", "attributes", "operator", "identifier_type"], "message": "..." }
        ]
      }
    }
    

    activity_not_allowed_for_operator_country anchors to data.attributes.operator.country instead of identifier_type. The other 422 arms on this endpoint (the DOMESTIC/country contradiction, the supplementary-unit pair) name no single field and so omit details entirely — read code first, then details if present.

  • country_of_activity — EU-27 or XI (Northern Ireland) only (TRACES allow-list); lowercase input is uppercased. Only meaningful for IMPORT/EXPORT (it doubles as the border-cross country); null clears it back to "derive from operator.country at filing".

  • comment — free text, trimmed, max 1024 chars; null clears.

Clearing an HS-code blocker

Coffee policy note: TRACES acceptance accepts bare 0901 (IMPORT and EXPORT re-probed on 2026-10-19), and Epoch now accepts the bare heading alongside its five subheadings. Production has not been verified. Coffee acceptance fields and examples here describe the current fm-api policy.

A rubber record declared as 401290 reads hs_code_not_accepted_by_traces in meta.submission on Get DDS Record. The blocker's accepted_hs_codes lists 40129030. PATCH that code:

{ "data": { "type": "dds_record", "attributes": { "commodity": { "hs_code": "40129030" } } } }

The next GET no longer carries the blocker. The same PATCH clears hs_code_too_broad. supplementary_unit_required and supplementary_unit_qualifier_mismatch clear with a PATCH of commodity.supplementary_unit and supplementary_unit_qualifier. See HS codes TRACES accepts.

Per-field validation

All string fields are trimmed server-side; pure-whitespace input is rejected. The lengths and formats listed in Create Audit Trail apply identically here for any field the caller chooses to patch.

Top-level attributes keys that don't map to an editable field are rejected with a 400 (the attributes object is strict — a typo'd or non-editable key surfaces as an error rather than a silent no-op):

FieldWhere it lives instead
filterStamped at create time and immutable
commodity (top-level scope)Derived at create from the resolved plot set
country_codeSame as above
operator_role / associated_statementsStamped at record creation; no post-create writer (the authorisation endpoint records consent only)
tracesPopulated post-submission by the submit flow

Response

Returns the full record envelope (same shape as Get DDS Record) reflecting the post-patch state. updated_at is bumped to the request time. The current version's operator + commodity blocks reflect the merged result; unchanged fields show their existing value.

Errors

StatusCodeWhen
400BAD_REQUESTA provided field failed Zod validation (bad HS code, malformed email, unsupported supplementary_unit_qualifier, non-TRACES country_of_activity, unknown/non-editable attribute key, etc.)
401UNAUTHORIZEDMissing/invalid API key
404NOT_FOUNDRecord isn't visible to the caller
409CONFLICTCurrent version is mid- or post-submission
422OPERATOR_INCOMPLETEAn operator patch can't form a complete override (version resolves live from org defaults)
422VALIDATION_ERRORMerged state breaks a TRACES invariant — the supplementary-unit pair was split; DOMESTIC with a country of activity; IMPORT/EXPORT with a VAT-identified operator; non-EU operator on a non-IMPORT activity

Required scope: ingestions:write