What I learned sending EUDR due diligence statements to TRACES over SOAP
DEV Community

What I learned sending EUDR due diligence statements to TRACES over SOAP

From 30 December 2026, any large or medium company that places coffee, cocoa, timber, soy, rubber, palm oil or cattle (and their derived products) on the EU market needs a due diligence statement (DDS) under the EU Deforestation Regulation (EUDR, Regulation 2023/1115). Small and micro companies follow on 30 June 2027, except for timber.

The statement goes into TRACES, the European Commission's system, and the goods don't clear customs without its reference number. You can type a DDS into the TRACES web form. But if you import a container of timber every week, you want your ERP to do it.

TRACES has a machine-to-machine API for exactly that, and I spent the last few weeks building against it for Sourevo. Here is what the documentation didn't make obvious.

It's SOAP, with a WS-Security digest

The operator API is SOAP 1.1 with a WS-Security UsernameToken and a password digest. The digest itself is the classic one:

PasswordDigest = Base64( SHA-1( nonce + created + authenticationKey ) )

The trap is the word "nonce". In the digest you concatenate the raw nonce bytes, not the Base64 string you put in the <wsse:Nonce> element. If you hash the Base64 text, every call fails authentication and the error doesn't tell you why.

import { createHash, randomBytes } from 'node:crypto';
const nonce = randomBytes(16);
const created = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
const digest = createHash('sha1')
  .update(Buffer.concat([nonce, Buffer.from(created), Buffer.from(authKey)]))
  .digest('base64');
// <wsse:Nonce EncodingType="...#Base64Binary">{nonce.toString('base64')}</wsse:Nonce>

Two more details: created goes without milliseconds, and every request also carries a WebServiceClientId header. Its value is different in the test environment (eudr-test) and in production (eudr-repository), so it belongs in configuration, not in code.

Test against the right environment

There are two "acceptance" systems and it's easy to pick the wrong one. The general TRACES NT acceptance environment is not where EUDR lives. EUDR has its own, at acceptance.eudr.webcloud.ec.europa.eu, with its own operator registration and its own "Web Services Access" request. We registered in the general one first and lost a few days before the helpdesk pointed us to the right host.

The WSDL is not identical either. The EUDR acceptance contract has structured addresses (street, postalCode, city) where older material shows a single free-text field. Download the WSDL and XSD from the environment you will actually call and generate or validate against those.

Old API versions are switched off

Our first real submission came back with This API version has been discontinued. After the 2025 amendment to the regulation (Regulation 2025/2650), versions 1 and 2 of the submission service are gone. Version 4 is one service, EUDRDueDiligenceStatementServiceV4, that submits, amends, withdraws and retrieves.

Things that changed along the way and break code written against older examples:

  • retractDds is now withdrawDds.
  • The operator role only accepts OPERATOR or REPRESENTATIVE_OPERATOR. Traders no longer submit statements.
  • Referenced statements go in groupedDeclarations, by reference number.
  • Error codes moved. Version 1 put EUDR_SNAKE_CASE codes in <message>. Version 4 puts EUDR-KEBAB-CASE codes in <ID>. Parse both if you ever had a V1 client.
  • A lookup with no results is not an empty list. It's a NotFoundException fault, which you have to catch and turn into "nothing found".

Rules the XSD doesn't tell you

The schema validates the shape of the message. The business rules live on the server, and a few of them surprised us:

  • percentageEstimationOrDeviation is required for a DOMESTIC activity and rejected for IMPORT. Same field, same schema, opposite rules depending on another field.
  • For IMPORT and EXPORT, the operator needs an EORI number on its TRACES profile. Without it you get EUDR-OPERATOR-EORI-FOR-ACTIVITY-MISSING, even though the EORI isn't in your message at all. It's account setup, not payload.
  • A representative submitting for a client must send the client's street and country, or you get EUDR-BEHALF-OPERATOR-STREET-EMPTY and friends.
  • You can only amend or withdraw a statement while it's AVAILABLE. Right after submission it's SUBMITTED, and you have to wait.

That last one matters for the design. Submission is asynchronous: you get an identifier back, TRACES analyses the statement, and the reference number and verification number show up later. Your integration has to poll (or be told) before it can hand the reference number to customs.

Geolocation is GeoJSON inside XML, in Base64

Each statement carries the geolocation of the plots where the commodity was produced, as GeoJSON. Inside the SOAP message, that GeoJSON goes Base64-encoded in geometryGeojson. A plot can be a point or a polygon, but plots over four hectares need a polygon (cattle establishments are the exception).

Once the SOAP side works, the files are the next source of trouble, and they come from suppliers you don't control: rings that don't close, latitude and longitude swapped, self-intersecting polygons, a point where a polygon is required. We ended up building a free GeoJSON validator for TRACES that checks for those (and reads KML and CSV too) before the file goes into a statement. It doesn't store the file.

What we put in front of it

Nobody should have to write WS-Security headers to import coffee. So Sourevo exposes a REST API over all of this (suppliers, plots, batches, statements) with a few properties that I'd want from any API that talks to a slow, asynchronous government system:

  • Idempotency on every POST. Send an Idempotency-Key header and a retried request within 24 hours returns the original response with Idempotent-Replayed: true, instead of creating a second statement. ERPs retry; customs paperwork shouldn't double.
  • Your own IDs. Suppliers, products and batches take an externalRef (the ID in your ERP). Creating the same externalRef twice returns the existing record with duplicate: true, so a sync job can be rerun safely.
  • Webhooks instead of polling. When TRACES assigns the reference number, you get a dds.status_changed event. Each delivery is signed: X-Sourevo-Signature: t=<unix>, v1=<hex HMAC-SHA256(secret, "<t>.<body>")> Check the timestamp tolerance and compare the HMAC in constant time. Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2, 6, 12 and 24 hours, then marked as failed.
  • A test mode that hits the real test system. During a trial, the API submits to the EUDR acceptance environment, so you get real TRACES validation errors with no legal effect. The response header X-Sourevo-Mode tells you which environment you're in.

There's also an n8n community node (@francodesystems-npm/n8n-nodes-sourevo) if you'd rather wire this up without code.

Takeaways

  • Hash the raw nonce bytes, not their Base64.
  • Make sure you're in the EUDR acceptance environment, not the general TRACES one, and use its WSDL.
  • Use version 4. Anything older is switched off.
  • Expect business rules the schema can't express, and put account prerequisites (like the EORI) in your onboarding checklist.
  • Treat submission as asynchronous and design for the reference number arriving later.
  • Validate geometry before you send it. That's where most rejections come from.

If you're integrating an ERP with EUDR and hit something I didn't cover, tell me in the comments. The full API reference is in the Sourevo developer docs.

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.