Get DDS Record

Fetch a single dds_record with its current version summary and completeness counts.

Path params

ParamDescription
recordIdDDS record id

Visibility

Single rule: dds_record.organization_id โˆˆ ctx.effectiveOrgIds. A record produced under one org is invisible to another and returns 404 so existence isn't leaked across orgs.

Response

Same data.attributes shape as List DDS Records โ€” see that endpoint for the field-by-field reference. Single-record envelope (no meta.count); the meta object instead carries the per-mode pre-submission readiness signal โ€” meta.submission.submit and meta.submission.manual, each { can_submit, blockers } (see Submission readiness).

lifecycle_status โ€” the dashboard verdict

ยง8.1 dashboard lifecycle state. Seven values, derived from the audit log + TRACES round-trip status + the staleness flag (see dds_mitigation_event_log). This is the field a UI renders to tell the operator where the DDS sits in its lifecycle.

ValueMeaning
draftVersion exists; no submit-lifecycle event and completeness gates haven't all passed
awaiting_submissionDraft + completeness gates pass (ready to file, just hasn't been clicked yet)
submittingversion.submit_started is the latest submit-lifecycle event in the audit log
submittedversion.submitted / version.marked_submitted landed; TRACES has not yet returned a terminal status
acceptedTRACES returned ACCEPTED (GAP-13, not surfaced today)
rejectedTRACES returned REJECTED (GAP-13, not surfaced today)
staleSubmission landed but newer monitoring data has arrived since the snapshot (Article 10.5 review trigger, GAP-10, not surfaced today)

lifecycle_status โ‰  the report builder's computed_status.status. The report exposes a narrower five-state status field (draft | submitting | submitted | manually_submitted | submission_failed) that is the raw audit-log fact used for reconciliation. lifecycle_status composes that with TRACES state and staleness โ€” a submitted status can resolve to accepted / rejected / stale here, and a submission_failed status falls back to draft / awaiting_submission because the operator can retry. Reconcilers read status; UIs read lifecycle_status.

submitting_at / submitted_at

Companion timestamps to lifecycle_status. Both are derived from the audit log (the same version.submit_started / version.submitted / version.marked_submitted events that drive lifecycle_status), so they will never contradict the status:

  • submitting_at is non-null only when lifecycle_status is submitting โ€” carries the occurred_at of the latest version.submit_started.
  • submitted_at is non-null whenever lifecycle_status is submitted / accepted / rejected / stale โ€” carries the occurred_at of the latest version.submitted / version.marked_submitted.

unaccounted_plot_count

Count of plots that satisfy the record's stored filter today but are not yet in the current version snapshot โ€” i.e. plots that landed on the source asset_task after the version was cut and would qualify for inclusion if the filter were re-applied. Operationally: this is the trigger for cutting a v2.

  • 0 when no plots have been added since the snapshot, or when the record has no current version yet.
  • data.links.unaccounted_plots exposes the paginated sub-resource (Get Unaccounted Plots) that returns the actual rows.

Single record: returned by default. When you pass ?include=, it is returned only if the list names unaccounted_plot_count: its cost grows with the source upload's plot count, so a caller that picks its blocks pays for it only on request.

List view: opt-in. On the list endpoint (GET /regulations/eudr/audit-trail) the field is computed per-record and is expensive on large orgs, so it is off by default and omitted from each item. Request it explicitly with ?include=unaccounted_plot_count; the list links.self preserves the parameter so a followed/cached link returns the same projection.

operator / commodity_details

Echo of the operator and commodity blocks supplied at create time. Editable via Update DDS Record while no version has been submitted. See Create DDS Record for the field-by-field reference.

Note: the top-level attributes.commodity field is the record's stamped homogeneity scope (the single commodity string the resolved plot set collapsed to, e.g. "cocoa"). The attributes.commodity_details block โ€” product type, HS code, quantity, etc. โ€” is the per-shipment commodity metadata supplied at create time.

submission โ€” the statement-level TRACES inputs

Read-only echo of the version's submission metadata (drives the TRACES filing). Populated on drafts too (since the editable subset became PATCHable โ€” GAP-23): pre-submit it reflects the staged values, including the create-time placeholders (activity_type IMPORT, operator_role OPERATOR until an authorisation sets it), and freezes at filing. Use lifecycle_status / traces to tell whether the record has been filed โ€” not this block's presence. This completes the V3-TRACES filing view that operator + commodity_details start.

FieldMeaning
activity_typeDOMESTIC | IMPORT | EXPORT
operator_roleOPERATOR or REPRESENTATIVE_OPERATOR (ยง9.1)
internal_reference_numberCaller's own reference for the filing
geo_location_confidentialWhether geolocation is filed confidentially
commentFree-text comment carried into the DDS
country_of_activityEU country the activity is filed under; null falls back to operator.country at submit
border_cross_countryMirrors country_of_activity, emitted only for IMPORT/EXPORT; null for DOMESTIC
associated_statementsReferenced upstream DDS {reference_number, verification_number} pairs

null only when the record has no current version.

internal_reference_number is also surfaced at the top level of data.attributes (kept for back-compat โ€” it predates the always-populated submission block, which now echoes the same value). It can be staged via Update DDS Record โ€” as can activity_type, country_of_activity, geo_location_confidential, and comment. It is optional; when left unset the submit flow auto-generates a stable E-<version-uuid> fallback at filing time.

Submission readiness โ€” meta.submission

Surfaced on the envelope meta (single-record getter only โ€” the list endpoint's meta stays pagination-only). Lets a client see what is blocking a filing without attempting a submit; it runs the same findPreSubmissionBlockers gate the submit path (POST .../submit) runs.

Readiness is reported per filing mode under meta.submission:

  • meta.submission.submit โ€” predicts a mode: "submit" (Epoch files to TRACES on the operator's behalf) filing.
  • meta.submission.manual โ€” predicts a mode: "manual" filing (the operator filed via the EU portal directly). Manual mode drops the submit-only gates (today: authorization_missing), so a record fileable only via the portal can report manual.can_submit: true while submit.can_submit is still false.

Each mode carries:

  • blockers โ€” array of reasons that mode can't be filed. Empty [] means that mode's pre-submission gate is clear. Each entry is a discriminated union on type with a human-readable message plus type-specific detail fields.
  • can_submit โ€” convenience boolean, true only when there is a current version, that mode's blockers is empty, and the version isn't already in/through submission (lifecycle_status not submitting/submitted/accepted). can_submit is derived solely from that mode's blocker list โ€” the exact gate the submit endpoint runs โ€” so it can't claim a mode is fileable when the submit call would reject it, nor block a mode the submit call would accept.
typeFires whenKey detail fields
plots_missing_screeningIncluded plots have no pinned deforestation metric (haven't passed ยง6 full-resolution screening)plot_ids
plots_unresolved_verdictIncluded plots have a pinned-but-indeterminate metric โ†’ null verdictplot_ids, total_count, truncated_count
plots_non_compliantEvery included plot is non-compliant, so nothing would remain to file. Non-compliant plots are auto-excluded at submit (compliant-only default), so a mix of compliant + non-compliant does not block โ€” this fires only when the auto-exclude would empty the filing. Because those plots will be dropped, they are also subtracted from the per-plot defect blockers below (unmitigated_article10_risk, plots_geometry_invalid, plots_missing_geometry) โ€” a plot destined for auto-exclusion never blocks the filing on its own sanctions/geometry defectplot_ids, total_count, truncated_count
plots_missing_geometryAn included plot has NULL geometry (TRACES requires geometry on every producer)plot_ids, total_count, truncated_count
plots_geometry_invalidPoint-geometry plots violate the ยง10 area_ha rules โ€” must be 0.1โ€“4 ha; cattle (HS chapters 01/02) are exempt from the 4 ha max but still need area_ha โ‰ฅ 0.1 haplot_ids, reasons (per-plot cause), total_count, truncated_count
too_many_producersIncluded suppliers exceed the ยง15 per-commodity producer cap (1000)producer_count, cap
commodity_missingAny of product_type / product_description / product_quantity_kg / hs_code is unset (commodity is optional at create, mandatory to file)missing_fields
operator_incompleteThe resolved operator (override, else org defaults) is missing TRACES-mandatory fieldsmissing_fields (operator_*)
country_of_activity_invalidThe resolved country of activity (submission override for IMPORT/EXPORT, else operator country) is not in EU-27 + XIcountry
operator_country_invalidThe operator's own country is not a recognised ISO 3166-1 alpha-2 code (e.g. ZZ). Only fires when it isn't the resolved country of activity (that case is reported as country_of_activity_invalid), so a bogus operator country on an IMPORT/EXPORT with a valid country-of-activity override still blocksoperator_country
operator_identifier_invalidGAP-25: activity_type is IMPORT/EXPORT but the resolved operator is VAT-identified โ€” TRACES requires EORI on cross-border filings. The record PATCH rejects an edit that creates this combination; this blocker is the backstop for pre-existing (e.g. create-time default) combinations the change-scoped edit gate toleratesactivity_type, identifier_type
activity_not_allowed_for_operator_countryGAP-26: the resolved operator's country is outside EU-27 + XI (Northern Ireland counts as EU for goods under the Windsor Framework) but activity_type is not IMPORT โ€” a non-EU operator may only file an IMPORT DDS. Same edit-gate/backstop pairing as operator_identifier_invalidactivity_type, operator_country
invalid_producer_countryThe record's country of production (country_code, derived from the plots and carried on every producer) is not a well-formed ISO alpha-2 codecountry
species_missingA timber / timber-derived commodity (HS chapters 44, 47) has species_scientific_name and/or species_common_name unset โ€” ยง13 requires species for wood (non-timber commodities don't); checked once the base commodity is populatedmissing_fields
species_name_unresolvedA timber commodity's declared species_scientific_name is not an accepted binomial TRACES will value-validate โ€” a malformed non-binomial (bare epithet, digits, or a rank placeholder such as Quercus sp / Various species) an unaccepted_synonym of an accepted species, or a non_canonical_name (the binomial resolves, but the declared string carries extra tokens โ€” an author authority, an infraspecific tail, or a second species joined by a word โ€” and the V3 mapper files that string verbatim) โ€” resolved against the curated GBIF-authoritative timber snapshot (species_taxon_*, also served by GET /regulations/eudr/species). A structurally-plausible-but-unlisted name and hybrids pass in v1. Distinct from species_missing (which is presence/shape only), and only runs on names that passed that gate, so a blank/blob name is never double-reportedissues (per-name index, declared, reason, suggestion โ€” the accepted binomial for a synonym or a non-canonical name, else null), truncated_count
commodity_description_too_longproduct_description exceeds the TRACES V3 descriptionOfGoods cap (150 chars)length, cap
out_of_scope_hs_codehs_code classifies outside EUDR Annex I scope and no horizontal exemption is claimedhs_code, matched_cn_code
hs_code_too_broadhs_code is in scope but broader than its Annex I entry (matched_by: "broader") and has fewer than 6 digits (FAQ 7.24), and no horizontal exemption is claimed. Message: hs_code <code> is broader than its EUDR Annex I entry; declare it to the Annex I digit level (at most 6 digits) via PATCH /regulations/eudr/audit-trail/:recordId before submittinghs_code
hs_code_not_accepted_by_traceshs_code is in scope but fm-apiโ€™s reviewed acceptance policy does not accept it, and no shorter form of it is accepted (e.g. rubber 401290). Clear it by PATCHing commodity.hs_code to one of accepted_hs_codes; when that list is empty, the product can't be filed through TRACES today. Not waived by an exemption claimhs_code, wire_hs_code, accepted_hs_codes ({ code, description })
supplementary_unit_requiredIMPORT/EXPORT, and TRACES requires a supplementary unit on the heading sent (MTQ on 4403, 4406, 4408, 4410โ€“4413; NAR on 4011, 4013; KSD on 4701, 4702, 4704, 4705) but none is stored. Clear it by PATCHing commodity.supplementary_unit and supplementary_unit_qualifier. Not waived by an exemption claimhs_code, wire_hs_code, heading, qualifier
supplementary_unit_qualifier_mismatchIMPORT/EXPORT, and the stored supplementary unit is not the one TRACES requires on the heading sent. Clear it by PATCHing the qualifier and the amount. Not waived by an exemption claimhs_code, wire_hs_code, heading, qualifier, stored_qualifier
unmitigated_article10_riskIncluded plots carry an unmitigated Article 10 "stop-the-line" risk (e.g. sanctions exposure)plot_ids, risk_categories
self_referencing_ddsThe version's own TRACES reference (from its filing statement, EPO-1816) appears in associated_statements (ยง16 "a DDS cannot reference itself")own_reference_number
authorization_missingยง9 step 3: no current-text-version operator authorisation for on-behalf-of filing (requireAuthorisation only)text_version
attachment_awaiting_verificationEPO-1448 ยง3.8: non-archived operator-evidence attachments on the version are still awaiting verification (verification_status โ‰  approved). No-op today (attachments default to approved)attachment_ids, filenames
engagement_threads_openReserved (GAP-5, engagement domain not implemented) โ€” always empty todaythread_ids

Parity with the submit gate. Each mode's list is produced by the same findPreSubmissionBlockers the submit endpoint runs, so meta.submission.<mode>.can_submit + blockers predict that mode's submit outcome (an empty list + can_submit: true means the submit will pass its pre-flight gates). submit is the superset; manual drops the submit-only gates (authorization_missing). The internal reference is not a blocker in either mode โ€” it is optional and auto-generated at submit when unset. Detail arms use passthrough, so new detail fields can appear on a blocker without a client change.

Example hs_code_not_accepted_by_traces blocker for a rubber record declared as 401290:

{
  "type": "hs_code_not_accepted_by_traces",
  "hs_code": "401290",
  "wire_hs_code": "401290",
  "accepted_hs_codes": [
    {
      "code": "40129030",
      "description": "Solid or cushion tyres, tyre treads and tyre flaps, of rubber"
    }
  ],
  "message": "TRACES does not accept hs_code 401290. Declare one of 40129030 via PATCH /regulations/eudr/audit-trail/:recordId before submitting."
}

A horizontal exemption (commodity.exemption_claim) waives only the Annex-scope blockers (out_of_scope_hs_code, hs_code_too_broad). The TRACES-wire blockers above still fire, because the filing still goes to TRACES. See HS codes TRACES accepts.

meta.submission.advisories and meta.submission.wire_hs_code

wire_hs_code is the HS code a filing sends to TRACES for this record (e.g. 010229 โ†’ 0102, 180100 โ†’ 1801), or null when the record has no hs_code or no scope version resolves. If you file via the EU portal (mode: "manual"), enter this value.

advisories are non-blocking heads-ups shared by both modes. They never affect can_submit. Each is { type, hs_code, wire_hs_code, message }.

typeFires whenExample message
hs_code_normalisedfm-api sends a shorter code than the one stored, because TRACES accepts only the shorter formTRACES accepts this product only as 0102; hs_code 010229 will be submitted to TRACES as 0102. Confirm 0102 describes your product.
hs_code_normalisedThe stored code is not 2, 4 or 6 digitshs_code 44031 is not a 2-, 4- or 6-digit HS code; it will be submitted to TRACES as 4403.
hs_code_normalisedThe stored code has a zero tailhs_code 180100 will be submitted to TRACES as its Annex I heading 1801 (CN 1801); confirm this heading still describes your product.
supplementary_unit_droppedIMPORT/EXPORT with a supplementary unit stored, TRACES accepts the code, and the heading sent is one where TRACES refuses a unit; the unit is not sentTRACES does not accept a supplementary unit on 4407; the supplementary unit (1.5 MTQ) will not be submitted. The net weight is still filed.
low_confidence_hs_codeTRACES does not accept the code, and it is broader than its Annex entry, matches only a chapter, matches no entry, or is in scope under an 8-digit Annex entry (cut to 6 digits on the wire)hs_code 1201 will be submitted to TRACES as 1201, which TRACES may reject (EUDR_COMMODITIES_HS_CODE_INVALID). Supply a CN heading/subheading TRACES recognises to be certain.

Advisories are computed on every read from the current version, so they stay visible after filing. The filed values themselves are on traces_statements[].lines.

meta.editable_fields โ€” the PATCH form descriptor

A self-describing list of every field Update DDS Record accepts, so a UI can render the edit form (and its validation) straight from this response instead of hard-coding it. Static per deployment. Each entry:

KeyMeaning
nameDotted path within data.attributes (e.g. commodity.hs_code)
labelHuman-readable field label
typestring | number | boolean | enum
nullableWhether null is an accepted value (clears the field)
optionsAllowed values for enum types (else null)
max_lengthCharacter cap for string fields (else null)
requiredBase requiredness (when required_when is null)
show_whenOnly render the field when every clause holds (else always)
required_whenRequired exactly when every clause holds (overrides required)

show_when / required_when are AND-ed lists of clauses { field, operator, value }. Operators: in (value โˆˆ array), equals, is_set (referenced field is non-null), hs_chapter_in (first two HS digits โˆˆ array), hs_prefix_in (HS digits start with an array entry, or are a broader prefix of one). The conditions encode the TRACES rules:

  • species (species_scientific_name / species_common_name) โ€” shown and required only for timber (hs_prefix_in over the Annex I timber prefixes: 4401-4421, 47, 48, 49, 940131-940191 (wooden seats), 940330-940391, 940610).
  • supplementary-unit pair โ€” each half is required_when the other is_set.

Only name, operator.* and commodity.* appear โ€” the submission block is not editable via PATCH (it's finalised on the submit endpoint), so it is absent from this descriptor.

The descriptor is the shape of the form, not a live permission: whether the record is editable at all is governed by the audit lock (a submitted version makes PATCH return 409), and operator.* is all-or-nothing (patch the whole block).

traces_statements

TRACES post-submission references (EPO-1816). Sourced from the version's filing statement(s) in dds_submission_statement, resolved via the dds_submission_member โ†’ statement join โ€” the sole source of the issued identifiers (the former scalar dds_record_version.traces_* columns were dropped). Empty until the version is filed; a single element for a within-cap filing; 1..N when an oversized DDS was split across TRACES statements (EPO-821). Issued values are null until TRACES returns them asynchronously via getDds.

FieldSource
statement_indexdds_submission_statement.statement_index (0-based)
reference_numberdds_submission_statement.traces_reference (null until issued)
verification_numberdds_submission_statement.traces_verification_number (null until issued)
dds_uuiddds_submission_statement.traces_dds_uuid
linesWhat fm-api sent TRACES per commodity line: [{ hs_heading, supplementary_unit }], the wire HS code and the supplementary unit (null when none was sent). Empty for a manual filing or one filed before fm-api recorded lines

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing/invalid API key
404NOT_FOUNDRecord isn't visible to the caller

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. The examples show current fm-api behaviour, including blocker messages for codes that remain unsupported.

Required scope: ingestions:read