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:
retractDdsis nowwithdrawDds.- The operator role only accepts
OPERATORorREPRESENTATIVE_OPERATOR. Traders no longer submit statements. - Referenced statements go in
groupedDeclarations, by reference number. - Error codes moved. Version 1 put
EUDR_SNAKE_CASEcodes in<message>. Version 4 putsEUDR-KEBAB-CASEcodes in<ID>. Parse both if you ever had a V1 client. - A lookup with no results is not an empty list. It's a
NotFoundExceptionfault, 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:
percentageEstimationOrDeviationis required for aDOMESTICactivity and rejected forIMPORT. Same field, same schema, opposite rules depending on another field.- For
IMPORTandEXPORT, the operator needs an EORI number on its TRACES profile. Without it you getEUDR-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-EMPTYand friends. - You can only amend or withdraw a statement while it's
AVAILABLE. Right after submission it'sSUBMITTED, 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-Keyheader and a retried request within 24 hours returns the original response withIdempotent-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 sameexternalReftwice returns the existing record withduplicate: true, so a sync job can be rerun safely. - Webhooks instead of polling. When TRACES assigns the reference number, you get a
dds.status_changedevent. 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-Modetells 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.
Comments
No comments yet. Start the discussion.