npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

eudr-api-client

v2.1.1

Published

Enterprise-grade Node.js library for the EU Deforestation Regulation (EUDR) TRACES system. Provides full V3 API integration: Due Diligence Statements (DDS), Simplified Declarations (SD), and Declaration Verification. Legacy V1/V2 API clients are retained

Downloads

509

Readme

🌲 EUDR API Client

npm version License: AGPL v3 Node.js Version Test Status

Enterprise-grade Node.js library for EU Deforestation Regulation (EUDR) compliance Complete integration with the EUDR TRACES system — V3 API: Due Diligence Statements (DDS), Simplified Declarations (SD), and Declaration Verification.

⚠️ V1 / V2 are discontinued — use V3

The EUDR Information System now only accepts V3 requests. As of this writing, the acceptance environment rejects V1/V2 requests with a SOAP fault: "This API version has been discontinued. Please use the V3 API endpoints." V1 and V2 client code is no longer functional against the live system.

This README documents the V3 API first, since it's the only version that actually works. The V1/V2 client classes (EudrSubmissionClient, EudrSubmissionClientV2, EudrRetrievalClient, EudrRetrievalClientV2) remain in the library and are still described in this document, but only in the Legacy: V1 / V2 API Reference section at the bottom — kept for historical reference and for anyone migrating an old integration, not for new development.

Start new integrations here:

  • EudrSubmissionClientV3 / EudrRetrievalClientV3 — Due Diligence Statement (DDS)
  • EudrSimplifiedDeclarationClientV3 — Simplified Declaration (SD), for micro/small primary operators
  • EudrVerifyDeclarationClientV3 — Declaration verification, for downstream operators and traders

EUDR Systems

The EUDR system operates on two environments:

  • 🟢 Production (LIVE): https://eudr.webcloud.ec.europa.eu/tracesnt/

    • Purpose: Real submissions with legal value
    • Web Service Client ID: eudr
    • Use: Only for products to be placed on the market or exported after entry into application
    • Note: Submissions have legal value and can be subject to checks by Competent Authorities
  • 🟡 Acceptance (Training): https://acceptance.eudr.webcloud.ec.europa.eu/tracesnt/

    • Purpose: Training and familiarization platform
    • Web Service Client ID: eudr-test
    • Use: Testing and getting familiar with the system
    • Note: Submissions have no legal value

Why EUDR API Client?

The EU Deforestation Regulation (EUDR) requires operators and traders to submit Due Diligence Statements (or, for eligible micro/small primary operators, Simplified Declarations) for commodities like wood, cocoa, coffee, and more. This library provides:

  • ✅ Full V3 API Coverage - DDS submission/retrieval, Simplified Declaration, and Declaration Verification, all fully implemented
  • ✅ Production-Ready - Tested against the real EUDR acceptance environment
  • ✅ Well-Documented - Comprehensive documentation with real examples
  • ✅ Enterprise Features - Robust error handling, logging, and comprehensive validation
  • ✅ Easy Integration - Simple API with real-world examples
  • ✅ Smart Endpoint Management - Automatic endpoint generation for standard environments
  • ✅ Flexible Configuration - Manual endpoint override when needed
  • ✅ Flexible Array Fields - Array properties accept both single objects and arrays for maximum flexibility
  • ℹ️ V1/V2 retained for reference only - kept in the library and documented at the bottom of this README, but no longer functional against the live EUDR system

Table of Contents

Quick Start

Installation

npm install eudr-api-client

Basic Setup

const { EudrSubmissionClientV3 } = require('eudr-api-client');

// Initialize the V3 client with automatic endpoint generation
const client = new EudrSubmissionClientV3({
  username: 'your-username',
  password: 'your-password',
  // Use "eudr-test" for the EUDR Traces acceptance environment, "eudr-repository" for production
  webServiceClientId: 'eudr-test', // See the Configuration section below for details
  ssl: false // SSL configuration: true for secure (production), false for development
});

Submit your first DDS:

const result = await client.submitDds({
  operatorRole: 'OPERATOR', // 'OPERATOR' or 'REPRESENTATIVE_OPERATOR'
  statement: {
    internalReferenceNumber: 'REF-001', // optional in V3 - generated by the system if omitted
    activityType: 'DOMESTIC', // 'DOMESTIC' | 'IMPORT' | 'EXPORT' (no 'TRADE' in V3)
    countryOfActivity: 'HR',
    commodities: [{
      descriptors: {
        descriptionOfGoods: 'Domestic wood products',
        goodsMeasure: { netWeight: 20 }
      },
      hsHeading: '4401',
      speciesInfo: {
        scientificName: 'Fagus silvatica',
        commonName: 'European Beech'
      },
      producers: [{
        country: 'HR',
        name: 'Your Company Ltd.',
        // Base64-encoded GeoJSON (Point, Polygon, MultiPolygon, ...)
        geometryGeojson: 'BASE64_ENCODED_GEOJSON'
      }]
    }],
    geoLocationConfidential: false
  }
});

console.log('✅ DDS Submitted. UUID:', result.uuid);

Configuration

Environment Variables

The library reads only EUDR_LOG_LEVEL from the environment. Credentials are not picked up automatically — pass them to the client constructor yourself. The variable names below are a suggested convention, not something the package resolves on your behalf.

# Read by the library
EUDR_LOG_LEVEL=info  # trace, debug, info, warn, error, fatal

# Your own convention - you wire these into the client config (see below)
EUDR_TRACES_USERNAME=your-username
EUDR_TRACES_PASSWORD=your-password
EUDR_WEB_SERVICE_CLIENT_ID=eudr-test
EUDR_SSL_ENABLED=false  # true for production (secure), false for development
require('dotenv').config();

const client = new EudrSubmissionClientV3({
  username: process.env.EUDR_TRACES_USERNAME,
  password: process.env.EUDR_TRACES_PASSWORD,
  webServiceClientId: process.env.EUDR_WEB_SERVICE_CLIENT_ID,
  ssl: process.env.EUDR_SSL_ENABLED === 'true'
});

Configuration Options

Automatic Endpoint Generation (Recommended):

const config = {
  // Required
  username: 'your-username',
  password: 'your-password',
  webServiceClientId: 'eudr-test', // Automatically generates the acceptance endpoint
  
  // Optional
  ssl: false, // true for production (secure), false for development
  timestampValidity: 60, // seconds
  timeout: 10000, // milliseconds
  bodyIdentity: 'OP12345678', // only for API users belonging to several operators - see Multi-Operator Authentication
};

Manual Endpoint Override (Advanced):

const config = {
  // Required
  endpoint: 'https://custom-endpoint.com/ws/EUDRDueDiligenceStatementServiceV3',
  username: 'your-username',
  password: 'your-password',
  webServiceClientId: 'custom-client', // Custom ID requires manual endpoint
  
  // Optional
  ssl: true, // true for production (secure), false for development
  timestampValidity: 60,
  timeout: 10000,
};

Multi-Operator Authentication

Requires EUDR Information System release 8.2.1 or later.

An API user that belongs to more than one EUDR operator can declare which operator it acts as on each call, using the optional BodyIdentity SOAP header. Before 8.2.1 a web service user could only belong to a single operator, and calls from a multi-operator user were rejected with EUDR_WEBSERVICE_USER_FROM_MANY_OPERATOR.

The value is the operator's Web Service Identifier, assigned by the Commission when the operator requests API access (max 16 characters; 32 for otherBodyAccessIdentifier).

// Configure a default identity for every call from this client
const client = new EudrSubmissionClientV3({
  username: 'your-username',
  password: 'your-password',
  webServiceClientId: 'eudr-test',
  bodyIdentity: 'OP12345678'          // shorthand for OperatorAccessIdentifier
});

// Or switch identity per call - the natural multi-operator pattern
await client.submitDds(request, { bodyIdentity: 'OP12345678' });
await client.submitDds(otherRequest, { bodyIdentity: 'OP87654321' });

// Pass null to suppress a configured identity for one call
await client.submitDds(request, { bodyIdentity: null });

Bodies other than operators use the object form, which accepts exactly one identifier:

{ bodyIdentity: { authorityActivityAccessIdentifier: 'AA-000123' } }
{ bodyIdentity: { organicControlBodyAccessIdentifier: 'OCB-00042' } }
{ bodyIdentity: { otherBodyAccessIdentifier: 'CUSTOMS-01' } }

| Aspect | Behaviour | |--------|-----------| | Supported clients | EudrSubmissionClientV3, EudrRetrievalClientV3, EudrSimplifiedDeclarationClientV3 (all 12 DDS + SD operations) | | Not supported | EudrVerifyDeclarationClientV3 — the Verify Declaration WSDL does not declare the header | | When omitted | No header is sent and the request is byte-identical to previous library versions — single-operator users need to change nothing | | Validation | Exactly one identifier kind (EUDR_V3_BODY_IDENTITY_INVALID), at most 16 characters — 32 for otherBodyAccessIdentifier (EUDR_V3_BODY_IDENTITY_TOO_LONG) | | Rejected identity | An identifier the account may not act as comes back as UnauthenticatedException — surfaced as error.httpStatus === 401 |

Configuration Priority

Priority Order for Endpoint Resolution:

  1. Manual endpoint (if provided) → Uses specified endpoint
  2. Standard webServiceClientId → Automatically generates endpoint
  3. Custom webServiceClientId → Requires manual endpoint configuration

What happens automatically:

  • webServiceClientId: 'eudr-repository' → Uses production environment
  • webServiceClientId: 'eudr-test' → Uses acceptance environment
  • Custom webServiceClientId → Requires manual endpoint configuration

Example Configuration Scenarios

const { EudrSubmissionClientV3 } = require('eudr-api-client');

// Scenario 1: Automatic endpoint generation (Recommended)
const autoClient = new EudrSubmissionClientV3({
  username: 'user',
  password: 'pass',
  webServiceClientId: 'eudr-test', // Automatically generates acceptance endpoint
  ssl: false // Development environment - allow self-signed certificates
});

// Scenario 2: Manual endpoint override
const manualClient = new EudrSubmissionClientV3({
  endpoint: 'https://custom-server.com/ws/EUDRDueDiligenceStatementServiceV3',
  username: 'user',
  password: 'pass',
  webServiceClientId: 'custom-id',
  ssl: false // Custom development server
});

// Scenario 3: Production environment
const productionClient = new EudrSubmissionClientV3({
  username: 'user',
  password: 'pass',
  webServiceClientId: 'eudr-repository', // Automatically generates production endpoint
  ssl: true // Production environment - validate SSL certificates
});

Accessing Configuration Information

You can access endpoint configuration information through the config export:

const { config } = require('eudr-api-client');

// Get supported client IDs
const supportedIds = config.getSupportedClientIds();
console.log('Supported IDs:', supportedIds); // ['eudr-repository', 'eudr-test']

// Get supported services
const supportedServices = config.getSupportedServices();
console.log('Supported Services:', supportedServices);

// Get supported versions for a service
const submissionVersions = config.getSupportedVersions('submission');
console.log('Submission Service Versions:', submissionVersions); // ['v1', 'v2', 'v3']

// Check if a client ID is standard
const isStandard = config.isStandardClientId('eudr-repository');
console.log('Is eudr-repository standard?', isStandard); // true

// Generate endpoint manually (if needed)
const endpoint = config.generateEndpoint('submission', 'v3', 'eudr-test');
console.log('Generated endpoint:', endpoint);

Real-World Examples

Import Operations

Scenario: Importing wood products with producer geolocation data

const { EudrSubmissionClientV3 } = require('eudr-api-client');

const importClient = new EudrSubmissionClientV3({
  username: process.env.EUDR_USERNAME,
  password: process.env.EUDR_PASSWORD,
  webServiceClientId: 'eudr-test', // Automatically generates acceptance endpoint
  ssl: false // Development environment - allow self-signed certificates
});

const importResult = await importClient.submitDds({
  operatorRole: 'OPERATOR',
  statement: {
    internalReferenceNumber: 'DLE20/359',
    activityType: 'IMPORT',
    countryOfActivity: 'HR',
    borderCrossCountry: 'HR',
    comment: 'Import with geolocations',
    commodities: [{
      descriptors: {
        descriptionOfGoods: 'Imported wood products from France',
        goodsMeasure: {
          netWeight: 300, // mandatory for IMPORT/EXPORT
          supplementaryUnit: 20,
          supplementaryUnitQualifier: 'MTQ' // cubic meters
        }
      },
      hsHeading: '4401',
      speciesInfo: {
        scientificName: 'Fagus silvatica',
        commonName: 'European Beech'
      },
      producers: [{
        country: 'FR',
        name: 'French Wood Producer',
        // Base64-encoded GeoJSON polygon
        geometryGeojson: 'BASE64_ENCODED_GEOJSON'
      }]
    }],
    geoLocationConfidential: false
  }
});

console.log(`✅ Import DDS submitted. UUID: ${importResult.uuid}`);

Domestic Production

Scenario: Domestic wood production with multiple species

const domesticResult = await client.submitDds({
  operatorRole: 'OPERATOR',
  statement: {
    internalReferenceNumber: 'DLE20/357',
    activityType: 'DOMESTIC',
    countryOfActivity: 'HR',
    commodities: [
      {
        position: 1,
        descriptors: {
          descriptionOfGoods: 'Prostorno drvo s glavnog stovarišta - BUKVA OBIČNA',
          goodsMeasure: { netWeight: 16, supplementaryUnit: 20, supplementaryUnitQualifier: 'MTQ' }
        },
        hsHeading: '4401',
        speciesInfo: {
          scientificName: 'Fagus silvatica',
          commonName: 'BUKVA OBIČNA'
        },
        producers: [{
          country: 'HR',
          name: 'GreenWood Solutions Ltd.',
          geometryGeojson: 'BASE64_ENCODED_GEOJSON'
        }]
      },
      {
        position: 2,
        descriptors: {
          descriptionOfGoods: 'Prostorno drvo s glavnog stovarišta - BUKVA OSTALE',
          goodsMeasure: { netWeight: 12, supplementaryUnit: 15, supplementaryUnitQualifier: 'MTQ' }
        },
        hsHeading: '4401',
        speciesInfo: {
          scientificName: 'Fagus sp.',
          commonName: 'BUKVA OSTALE'
        },
        producers: [{
          country: 'HR',
          name: 'GreenWood Solutions Ltd.',
          geometryGeojson: 'BASE64_ENCODED_GEOJSON'
        }]
      }
    ],
    geoLocationConfidential: false
  }
});

console.log(`✅ Domestic DDS submitted. UUID: ${domesticResult.uuid}`);

Grouped Declarations

Scenario: Submitting a new DDS that references previously submitted DDS or SD declarations for grouping. This is the V3 replacement for the old V1/V2 associatedStatements/TRADE pattern — see Data Types for the conceptual difference.

Terminology: what the TRACES NT web interface calls a Group Head is exactly this — a DDS or SD submitted with groupedDeclarations. There is no separate group-head API operation or field; the grouping declaration gets its own reference number, and its members move to GROUPED status. A DDS group head may group DDS and/or SD members; an SD group head may group SD members only. The schema allows up to 2000 references per submission (the library rejects more with EUDR_V3_GROUPED_DECLARATIONS_LIMIT), while the Commission's release notes quote a business limit of 1000 members per group.

const groupedResult = await client.submitDds({
  operatorRole: 'OPERATOR',
  statement: {
    internalReferenceNumber: 'GROUPED-REF-001',
    activityType: 'IMPORT',
    countryOfActivity: 'BE',
    borderCrossCountry: 'BE',
    commodities: [{
      descriptors: {
        descriptionOfGoods: 'Grouped cocoa shipment',
        goodsMeasure: { netWeight: 5000 }
      },
      hsHeading: '1801',
      speciesInfo: {
        scientificName: 'Theobroma cacao',
        commonName: 'Cacao'
      },
      producers: [{
        country: 'BR',
        name: 'Producer Name',
        geometryGeojson: 'BASE64_ENCODED_GEOJSON'
      }]
    }],
    geoLocationConfidential: false,
    // References to previously submitted DDS/SD reference numbers
    groupedDeclarations: [
      { groupedDeclaration: '26FRYUI34JTQKB' },
      { groupedDeclaration: '26FRLSCV861ZVV' }
    ]
  }
});

console.log(`✅ Grouped DDS submitted. UUID: ${groupedResult.uuid}`);

Note: referenced declarations receive GROUPED status and can no longer be individually amended/withdrawn while the grouping declaration is active. Submission is blocked if any referenced statement is not in AVAILABLE status. This is not the same concept as V1/V2's associatedStatements — see V3 DDS Facade Clients.

Authorized Representatives

Scenario: Submitting on behalf of another operator (REPRESENTATIVE_OPERATOR role)

const representativeResult = await client.submitDds({
  operatorRole: 'REPRESENTATIVE_OPERATOR',
  statement: {
    internalReferenceNumber: 'DLE20/360',
    activityType: 'IMPORT',
    representedOperator: {
      // EconomicOperatorReferenceNumberType - structured identifier
      operatorReferenceNumber: {
        identifierType: 'eori', // eori | vat | gln | tin | cbr | cin | duns | comp_num | comp_reg | oni
        identifierValue: 'HR123456789'
      },
      // AddressType - structured address (country/street/postalCode/city all required if provided)
      operatorAddress: {
        country: 'HR',
        street: 'Ulica Kneza Branimira 2',
        postalCode: '10000',
        city: 'Zagreb'
      },
      operatorEmail: '[email protected]',
      operatorPhone: '+385 (001) 480-4111',
      operatorName: 'Croatian Import Company' // mandatory
    },
    countryOfActivity: 'HR',
    borderCrossCountry: 'HR',
    comment: 'Import by authorized representative',
    commodities: [{
      descriptors: {
        descriptionOfGoods: 'Wood products imported by representative',
        goodsMeasure: { netWeight: 250, supplementaryUnit: 12, supplementaryUnitQualifier: 'MTQ' }
      },
      hsHeading: '4401',
      speciesInfo: {
        scientificName: 'Fagus silvatica',
        commonName: 'European Beech'
      },
      producers: [{
        country: 'GH',
        name: 'Ghana Wood Board',
        geometryGeojson: 'BASE64_ENCODED_GEOJSON'
      }]
    }],
    geoLocationConfidential: false
  }
});

console.log(`✅ Representative DDS submitted. UUID: ${representativeResult.uuid}`);

Business Rules & Validation

Unlike the legacy V2 client (see Legacy FAQ), the V3 clients do not pre-validate units-of-measure business rules client-side. V3 leans on the server for this: submit the request, and if a rule is violated the server returns a BusinessRulesValidationException SOAP fault, which EudrErrorHandler surfaces as a structured error.

The V3 clients do validate, client-side and before any network call, the things that are structural/schema-level rather than business rules — for example:

try {
  await client.submitDds({
    operatorRole: 'OPERATOR',
    statement: { activityType: 'TRADE' /* not supported in V3 */, /* ... */ }
  });
} catch (error) {
  console.error(error.eudrErrorCode); // 'EUDR_V3_ACTIVITY_TYPE_TRADE_NOT_SUPPORTED'
}

Client-side validation error codes (V3)

All of these are thrown before any network call, carry error.eudrSpecific === true, and are the same in every V3 client unless the table says otherwise.

| Error code | When it's thrown | |---|---| | EUDR_V3_ACTIVITY_TYPE_TRADE_NOT_SUPPORTED | activityType: 'TRADE' — dropped in V3 (DDS) | | EUDR_V3_ACTIVITY_TYPE_INVALID | activityType is not DOMESTIC/IMPORT/EXPORT (DDS) | | EUDR_V3_OPERATOR_ROLE_INVALID | operatorRole is not OPERATOR/REPRESENTATIVE_OPERATOR (DDS) | | EUDR_V3_LEGACY_OPERATOR_TYPE_FIELD | the V1/V2 operatorType field was passed | | EUDR_V3_LEGACY_ASSOCIATED_STATEMENTS_FIELD | the V1/V2 associatedStatements field was passed | | EUDR_V3_IDENTIFIER_TYPE_INVALID | operatorReferenceNumber.identifierType is outside the V3 enum | | EUDR_V3_OPERATOR_REFERENCE_NUMBER_LIMIT | more than 12 operatorReferenceNumber entries | | EUDR_V3_GROUPED_DECLARATIONS_LIMIT | more than 2000 groupedDeclarations references | | EUDR_V3_COMMODITIES_LIMIT | more than 200 commodities (same cap in DDS and SD) | | EUDR_V3_PRODUCERS_LIMIT | more than 1000 producers on one commodity (same cap in DDS and SD) | | EUDR_V3_SPECIES_INFO_LIMIT | more than 500 speciesInfo entries on one commodity (DDS only — SD has no speciesInfo) | | EUDR_V3_DESCRIPTION_OF_GOODS_REQUIRED | descriptors was provided without descriptionOfGoods — both children of CommercialDescriptionType are mandatory | | EUDR_V3_GOODS_MEASURE_REQUIRED | descriptors was provided without goodsMeasure | | EUDR_V3_GEOJSON_INVALID | geometryGeojson is neither a base64 string, a Buffer, nor a GeoJSON object | | EUDR_V3_BODY_IDENTITY_INVALID | bodyIdentity has zero, several, or unknown identifier fields | | EUDR_V3_BODY_IDENTITY_TOO_LONG | a bodyIdentity value longer than its schema limit (16 characters; 32 for otherBodyAccessIdentifier) | | EUDR_V3_SD_* | SD-only rules — see the SD validation errors table |

How server faults are surfaced

EudrErrorHandler normalizes every SOAP fault into the same thrown Error. The raw fault always remains available on error.details.soapFault.

| Server fault | error.httpStatus | Also set | |---|---|---| | NotFoundException (V3 get* operations) | 404 | error.notFound === true, eudrErrorCode: 'EUDR_NOT_FOUND' | | UnauthenticatedException | 401 | — (wrong credentials, or a bodyIdentity the account may not act as) | | PermissionDeniedException / "not authorized" faults | 403 | — | | BusinessRulesValidationException | 400 | error.eudrErrors[] with { code, message, field } — code is null when the server reports no error code (the { field, message } fault shape used by Verify Declaration) | | XSD validation (SAXParseException / cvc-*) | 400 | eudrErrors[0].code === 'XML_VALIDATION_ERROR' |

NotFoundException became an explicitly declared fault on the V3 get* operations with EUDR release 8.2.1; before that it surfaced as a generic 500.

The faultcode prefix varies across the V3 services (S:, soapenv:, env:, SOAP-ENV:); the mapping above is prefix-agnostic. BusinessRulesValidationException is mapped by name because the EUDR system reports it with a Server faultcode.

See each V3 client's Error Handling subsection below for worked examples.

API Reference

Services Overview

🚀 All services support automatic endpoint generation!

⚠️ V1/V2 are discontinued on the live EUDR system. The acceptance environment rejects V1/V2 requests with a SOAP fault: "This API version has been discontinued. Please use the V3 API endpoints." V1/V2 client code remains in this library for historical/migration reference, but it is not functional against the live system. All new integrations must use the V3 clients.

| Service | Class | Automatic Endpoint | Manual Override | Specification | |---------|-------|-------------------|-----------------|------------------| | DDS Submission (V3) | EudrSubmissionClientV3 | ✅ Yes | ✅ Yes | Operator API v1.0 | | DDS Retrieval (V3) | EudrRetrievalClientV3 | ✅ Yes | ✅ Yes | Operator API v1.0 | | Simplified Declaration (V3) | EudrSimplifiedDeclarationClientV3 | ✅ Yes | ✅ Yes | Operator API v1.0 | | Verify Declaration (V3) | EudrVerifyDeclarationClientV3 | ✅ Yes | ✅ Yes | Downstream Operator & Trader API v1.0 | | Echo Service | EudrEchoClient | ✅ Yes | ✅ Yes | CF1 v1.4 | | Submission Service V1 ⚠️ non-functional | EudrSubmissionClient | ✅ Yes | ✅ Yes | CF2 v1.4 | | Submission Service V2 ⚠️ non-functional | EudrSubmissionClientV2 | ✅ Yes | ✅ Yes | CF2 v1.4 | | Retrieval Service V1 ⚠️ non-functional | EudrRetrievalClient | ✅ Yes | ✅ Yes | CF3 & CF7 v1.4 | | Retrieval Service V2 ⚠️ non-functional | EudrRetrievalClientV2 | ✅ Yes | ✅ Yes | CF3 & CF7 v1.4 |

Endpoint Generation Rules:

  • webServiceClientId: 'eudr-repository' → Production environment endpoints
  • webServiceClientId: 'eudr-test' → Acceptance environment endpoints
  • Custom webServiceClientId → Requires manual endpoint configuration

Example:

const {
  EudrSubmissionClientV3,
  EudrRetrievalClientV3,
  EudrSimplifiedDeclarationClientV3,
  EudrVerifyDeclarationClientV3,
  EudrEchoClient
} = require('eudr-api-client');

const echoClient = new EudrEchoClient({
  username: 'user', password: 'pass', webServiceClientId: 'eudr-test', ssl: false
});

const submissionV3Client = new EudrSubmissionClientV3({
  username: 'user', password: 'pass', webServiceClientId: 'eudr-test', ssl: false
});

const retrievalV3Client = new EudrRetrievalClientV3({
  username: 'user', password: 'pass', webServiceClientId: 'eudr-repository', ssl: true
});

const simplifiedDeclarationV3Client = new EudrSimplifiedDeclarationClientV3({
  username: 'user', password: 'pass', webServiceClientId: 'eudr-test', ssl: false
});

const verifyDeclarationV3Client = new EudrVerifyDeclarationClientV3({
  username: 'user', password: 'pass', webServiceClientId: 'eudr-test', ssl: false
});

For detailed endpoint configuration options, see the Configuration section.

Echo Service

Test connectivity and authentication with the EUDR system.

const { EudrEchoClient } = require('eudr-api-client');
const echoClient = new EudrEchoClient(config);

// Test connection
const response = await echoClient.echo('Hello EUDR');

console.log('Echo response:', response.status);

Methods

| Method | Description | Parameters | Returns | |--------|-------------|------------|---------| | echo(params) | Test service connectivity | message (String) | Promise with echo response |

Example

const response = await echoClient.echo('Hello EUDR');
// Returns: { message: 'Hello EUDR' }

V3 DDS Facade Clients

V3 is a new API contract, not a "V2.1." It is not backward compatible with V1/V2: field names, response shapes, operation names, and some business rules changed (see the breaking-changes table below). Treat migration to V3 like integrating a new API, not applying a patch.

V3 uses a single unified DDS backend service, but the library's public API is intentionally split into two facade clients for consistency with the existing pattern:

  • EudrSubmissionClientV3 for write operations (submitDds, amendDds, withdrawDds)
  • EudrRetrievalClientV3 for retrieval operations (getDds, getDdsByInternalReference, getDdsByIdentifiers)
const { EudrSubmissionClientV3, EudrRetrievalClientV3 } = require('eudr-api-client');

const submissionV3 = new EudrSubmissionClientV3({
  username: 'user',
  password: 'pass',
  webServiceClientId: 'eudr-test',
  ssl: false
});

const retrievalV3 = new EudrRetrievalClientV3({
  username: 'user',
  password: 'pass',
  webServiceClientId: 'eudr-repository',
  ssl: true
});

// Write operations
await submissionV3.submitDds({ /* V3 payload */ });
await submissionV3.amendDds('uuid', { /* V3 statement */ });
await submissionV3.withdrawDds('uuid');

// Retrieval operations
await retrievalV3.getDds('uuid'); // also accepts an array of up to 100 uuids
await retrievalV3.getDdsByInternalReference('INT-REF-001');
await retrievalV3.getDdsByIdentifiers('REFERENCE-NUMBER', 'VERIFICATION-NUMBER');

V3 retrieval response shapes:

  • getDds / getDdsByInternalReference return { httpStatus, status, ddsInfo: [...] } — ddsInfo is always an array of DDS overview entries (uuid, internalReferenceNumber, referenceNumber, verificationNumber, status, date, updatedBy, version, ...).
  • getDdsByIdentifiers returns { httpStatus, status, statement: {...} } — the full DDS statement (activityType, commodities, geoLocationConfidential, ...), not an overview list.

V1/V2 -> V3 breaking changes (no silent translation): V3 does not accept old V1/V2 field names — the library throws a clear, tagged error instead of guessing a mapping:

| Old (V1/V2) | New (V3) | If you still pass the old field | |---|---|---| | operatorType | operatorRole (OPERATOR | REPRESENTATIVE_OPERATOR) | throws EUDR_V3_LEGACY_OPERATOR_TYPE_FIELD | | activityType: 'TRADE' | not supported in V3 (DOMESTIC | IMPORT | EXPORT only) | throws EUDR_V3_ACTIVITY_TYPE_TRADE_NOT_SUPPORTED | | associatedStatements | groupedDeclarations: [{ groupedDeclaration: referenceNumber }] | throws EUDR_V3_LEGACY_ASSOCIATED_STATEMENTS_FIELD |

Note V3 grouping is not the same concept as V1/V2 referenced statements: a grouped declaration receives GROUPED status and can no longer be individually amended/withdrawn while the grouping declaration is active — that's why the library doesn't auto-translate associatedStatements.

try {
  await submissionV3.submitDds({ operatorType: 'TRADER', statement: { /* ... */ } });
} catch (error) {
  if (error.eudrErrorCode === 'EUDR_V3_LEGACY_OPERATOR_TYPE_FIELD') {
    console.error('Use operatorRole instead of operatorType in V3:', error.message);
  }
}

Migrating an old V1/V2 integration? See the full Legacy: V1 / V2 API Reference section at the bottom of this README for the old client examples and a side-by-side migration snippet.


V3 Simplified Declaration Client

Simplified Declaration (SD) is a new V3-only concept with no V1/V2 equivalent — it does not replace DDS, it's an alternative track for a specific operator category:

  • Use DDS (EudrSubmissionClientV3 / EudrRetrievalClientV3) for standard, per-shipment due diligence statements.
  • Use SD (EudrSimplifiedDeclarationClientV3) if you are a micro or small primary operator (natural person or micro/small undertaking) established in a low-risk country, placing on the market or exporting products you produced yourself. SD is a one-time declaration covering all your relevant products, submitted once instead of per shipment.

Unlike DDS, SD is exposed as a single unified client (no submission/retrieval split) since there's no pre-existing V1/V2 pattern to stay consistent with.

const { EudrSimplifiedDeclarationClientV3 } = require('eudr-api-client');

const sdClient = new EudrSimplifiedDeclarationClientV3({
  username: 'user',
  password: 'pass',
  webServiceClientId: 'eudr-test',
  ssl: false
});

// Submit a new Simplified Declaration
const submitResult = await sdClient.submitSd({
  operatorRole: 'MICRO_OPERATOR', // or REPRESENTATIVE_MSPO, MEMBER_STATE
  statement: {
    internalReferenceNumber: 'SD-REF-001', // mandatory for SD (optional for DDS)
    activityType: 'IMPORT', // DOMESTIC | IMPORT | EXPORT (no TRADE)
    commodities: [{
      descriptors: {
        descriptionOfGoods: 'Cocoa beans',
        goodsMeasure: { netWeight: 1000 }
      },
      hsHeading: '1801',
      producers: [{
        producerCountry: 'CI',
        producerName: 'Producer Name',
        producerLocation: {
          // exactly one of: geometryGeojson | postalAddress | cadastralIdentifier
          geometryGeojson: 'BASE64_ENCODED_GEOJSON'
        }
      }]
    }],
    geoLocationConfidential: false
  }
});
console.log(submitResult.sdIdentifier); // note: submit response field is `sdIdentifier`, not `uuid`

// Update / withdraw (identified by sdIdentifier)
const updateResult = await sdClient.updateSd(submitResult.sdIdentifier, { /* updated statement */ });
console.log(updateResult.uuid, updateResult.status); // note: update/withdraw responses use `uuid`, not `sdIdentifier`
await sdClient.withdrawSd(submitResult.sdIdentifier);

// Retrieval
await sdClient.getSd(submitResult.sdIdentifier); // also accepts [{ uuid, version }] or an array, up to 100
await sdClient.getSdByInternalReference('SD-REF-001');
await sdClient.getSdByIdentifiers('DECLARATION-IDENTIFIER', 'VERIFICATION-NUMBER');

Producer location alternatives (unlike DDS, GeoJSON is not mandatory for SD): a producer's location must be provided as exactly one of:

  • geometryGeojson — base64-encoded GeoJSON (same as DDS)
  • postalAddress — { producerStreet?, producerPostalCode, producerCity } (single object or array)
  • cadastralIdentifier — a land-registry identifier string (single value or array)

SD response shapes:

  • submitSd returns { httpStatus, status, sdIdentifier }.
  • updateSd / withdrawSd return { httpStatus, status, uuid, version, status: lifecycleStatus } (same EudrStatusType lifecycle values as DDS: SUBMITTED, AVAILABLE, REJECTED, WITHDRAWN, ARCHIVED, SUSPENDED, UPDATED, GROUPED, OBSOLETE).
  • getSd / getSdByInternalReference return { httpStatus, status, sdInfo: [...] } (array of SD overview entries, same shape family as DDS ddsInfo).
  • getSdByIdentifiers returns { httpStatus, status, statement: {...} } — the full SD statement. Note SD commodities have no speciesInfo field (unlike DDS).

SD validation errors (client-side, thrown before any network call — same error.eudrErrorCode / error.eudrSpecific pattern as DDS):

| Error code | When it's thrown | |---|---| | EUDR_V3_SD_OPERATOR_ROLE_INVALID | operatorRole is not one of MICRO_OPERATOR, REPRESENTATIVE_MSPO, MEMBER_STATE | | EUDR_V3_SD_INTERNAL_REFERENCE_REQUIRED | statement.internalReferenceNumber is missing (mandatory for SD) | | EUDR_V3_SD_INTERNAL_REFERENCE_TOO_LONG | statement.internalReferenceNumber is longer than 14 characters — see the note below | | EUDR_V3_SD_ACTIVITY_TYPE_INVALID | statement.activityType is not DOMESTIC, IMPORT, or EXPORT | | EUDR_V3_SD_PRODUCER_COUNTRY_REQUIRED | a producer is missing producerCountry | | EUDR_V3_SD_PRODUCER_LOCATION_INVALID | a producer's location has zero or more than one of geometryGeojson/postalAddress/cadastralIdentifier | | EUDR_V3_SD_REPRESENTED_OPERATOR_REQUIRED | operatorRole is REPRESENTATIVE_MSPO but statement.representedOperator is missing (submitSd only — updateSd carries no role) | | EUDR_V3_SD_POSTAL_ADDRESS_LIMIT | more than 100 postalAddress entries on one producer location | | EUDR_V3_SD_CADASTRAL_IDENTIFIER_LIMIT | more than 100 cadastralIdentifier entries on one producer location |

The SD client also throws the shared V3 codes — EUDR_V3_IDENTIFIER_TYPE_INVALID, EUDR_V3_OPERATOR_REFERENCE_NUMBER_LIMIT, EUDR_V3_GROUPED_DECLARATIONS_LIMIT, EUDR_V3_COMMODITIES_LIMIT, EUDR_V3_PRODUCERS_LIMIT, EUDR_V3_DESCRIPTION_OF_GOODS_REQUIRED, EUDR_V3_GOODS_MEASURE_REQUIRED, EUDR_V3_GEOJSON_INVALID, EUDR_V3_BODY_IDENTITY_INVALID, EUDR_V3_BODY_IDENTITY_TOO_LONG — listed in Client-side validation error codes.

internalReferenceNumber is capped at 14 characters for SD, not 50. The schema types the SD field as eudrCommon:ReferenceNumberType (maxLength 14), while the DDS field and the getSdByInternalReference lookup both use InternalReferenceNumberType (maxLength 50) — and the EU's 1.5 reference doc separately claims 35. Three numbers for one field; the library follows the type that actually carries it, so keep SD internal references at 14 characters or fewer.

submitSd returns a UUID, not the S… number. SubmitSdResponse/sdIdentifier is typed eudrCommon:UuidType, so it is a plain UUID and is what you pass to updateSd/withdrawSd/getSd. The 14-character declaration reference (e.g. S26FRNMNBSA96Q) that accompanies goods through the supply chain is referenceNumber in the getSd overview, so obtaining it takes a second call. The EU's own annotation blurs the two — build against the types.

geometryGeojson accepts more than a string. The schema type is xs:base64Binary, so the library base64-encodes a Buffer or a plain GeoJSON object for you; a string is passed through untouched, on the assumption it is already base64. This applies to the DDS client too.


🚀 EudrSubmissionClientV3

The V3 client for submitting, amending, and withdrawing DDS statements against the unified DDS V3 service. Not backward compatible with V1/V2 payloads — see the breaking-changes table in V3 DDS Facade Clients.

Methods

| Method | Description | Parameters | Returns | |--------|-------------|------------|---------| | submitDds(request, options) | Submit a new DDS (V3) | request (Object), options (Object) | Promise with uuid | | amendDds(uuid, statement, options) | Amend an existing DDS (V3) | uuid (String), statement (Object), options (Object) | Promise with uuid + lifecycle status | | withdrawDds(uuid, options) | Withdraw a DDS (V3, renamed from retractDds) | uuid (String), options (Object) | Promise with uuid + lifecycle status |

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | rawResponse | boolean | false | Whether to return the raw XML response instead of the parsed result | | bodyIdentity | string|Object | - | Operator identity for this call (multi-operator authentication); overrides the configured value, null suppresses it |

Detailed Method Reference

submitDds(request, options)

const result = await submissionV3.submitDds({
  operatorRole: 'OPERATOR',  // 'OPERATOR' or 'REPRESENTATIVE_OPERATOR' (renamed from operatorType)
  statement: {
    internalReferenceNumber: 'REF-001',  // optional in V3
    activityType: 'IMPORT',  // 'DOMESTIC' | 'IMPORT' | 'EXPORT' (TRADE removed in V3)
    countryOfActivity: 'HR',
    borderCrossCountry: 'HR',
    commodities: [{
      descriptors: {
        descriptionOfGoods: 'Wood products',
        goodsMeasure: { netWeight: 100 }
      },
      hsHeading: '4401',
      speciesInfo: { scientificName: 'Fagus silvatica', commonName: 'Beech' },
      producers: [{ country: 'HR', name: 'Producer Ltd.', geometryGeojson: 'BASE64_ENCODED_GEOJSON' }]
    }],
    geoLocationConfidential: false,
    groupedDeclarations: [{ groupedDeclaration: '25NLSN6LX69730' }]  // optional; replaces associatedStatements
  }
}, {
  rawResponse: false  // Set to true to get raw XML response
});

// Returns: { httpStatus: 200, status: 200, uuid: 'uuid-string', raw: 'xml...', parsed: {...} }

amendDds(uuid, statement, options)

const result = await submissionV3.amendDds(
  'existing-dds-uuid',
  {
    activityType: 'IMPORT',
    commodities: [ /* ... */ ],
    geoLocationConfidential: false
  },
  { rawResponse: false }
);

// Returns: { httpStatus: 200, status: 'AVAILABLE', uuid: 'existing-dds-uuid', raw: 'xml...' }
// Note: `status` here is the DDS lifecycle status (see EudrStatusType below), not the HTTP status code.
// The real HTTP status code is always available in `httpStatus`.

withdrawDds(uuid, options)

const result = await submissionV3.withdrawDds(
  'dds-uuid-to-withdraw',
  { rawResponse: false }
);

// Returns: { httpStatus: 200, status: 'WITHDRAWN', uuid: 'dds-uuid-to-withdraw' }

EudrStatusType lifecycle values (returned by amendDds/withdrawDds, and inside ddsInfo/statement from the retrieval client): SUBMITTED, AVAILABLE, REJECTED, WITHDRAWN, ARCHIVED, SUSPENDED (not active yet), UPDATED (not active yet), GROUPED, OBSOLETE.

Error Handling

try {
  const result = await submissionV3.submitDds({
    operatorRole: 'OPERATOR',
    statement: { activityType: 'IMPORT', /* ... */ }
  });
} catch (error) {
  // Client-side validation errors, thrown before any network call (see the breaking-changes table above)
  if (error.eudrErrorCode === 'EUDR_V3_OPERATOR_ROLE_INVALID') {
    console.error('Invalid operatorRole:', error.message);
  } else if (error.eudrErrorCode === 'EUDR_V3_ACTIVITY_TYPE_TRADE_NOT_SUPPORTED') {
    console.error('TRADE is not supported in V3:', error.message);
  } else if (error.eudrErrorCode?.startsWith('EUDR_V3_BODY_IDENTITY')) {
    console.error('Invalid bodyIdentity:', error.message);
  } else if (error.httpStatus === 401) {
    // Wrong credentials, or a bodyIdentity this account may not act as
    console.error('Authentication rejected:', error.details?.soapFault?.faultString);
  } else if (error.details?.soapFault) {
    // Server-side faults: BusinessRulesValidationException / PermissionDeniedException
    console.error('SOAP fault:', error.details.soapFault.faultString);
    console.error('Error details:', error.details.soapFault.errorDetails);
  } else {
    console.error('Unexpected error:', error.message);
  }
}

Configuration Examples

// Production environment with SSL validation
const productionV3Client = new EudrSubmissionClientV3({
  username: process.env.EUDR_USERNAME,
  password: process.env.EUDR_PASSWORD,
  webServiceClientId: 'eudr-repository',
  ssl: true,
  timeout: 30000
});

// Development environment with relaxed SSL
const devV3Client = new EudrSubmissionClientV3({
  username: process.env.EUDR_USERNAME,
  password: process.env.EUDR_PASSWORD,
  webServiceClientId: 'eudr-test',
  ssl: false,
  timeout: 10000
});

// Manual endpoint override
const customV3Client = new EudrSubmissionClientV3({
  endpoint: 'https://custom-endpoint.com/ws/EUDRDueDiligenceStatementServiceV3',
  username: 'user',
  password: 'pass',
  webServiceClientId: 'custom-client',
  ssl: false
});

🚀 EudrRetrievalClientV3 (V3)

Retrieval facade over the unified DDS V3 service. Unlike V1/V2, retrieval and submission share the same backend service — this client exists purely to keep the library's familiar submission/retrieval split.

Methods

| Method | Description | Parameters | Returns | |--------|-------------|------------|---------| | getDds(uuids, options) | Retrieve DDS overview by UUID(s), renamed from getDdsInfo | uuids (String or Array, max 100), options (Object) | Promise with ddsInfo array | | getDdsByInternalReference(internalReferenceNumber, options) | Retrieve DDS overview by internal reference, renamed from getDdsInfoByInternalReferenceNumber | internalReferenceNumber (String), options (Object) | Promise with ddsInfo array | | getDdsByIdentifiers(referenceNumber, verificationNumber, options) | Retrieve full DDS content, renamed from getStatementByIdentifiers | referenceNumber (String), verificationNumber (String), options (Object) | Promise with full statement | | ~~getReferencedDds()~~ | ❌ Not available in V3 — the spec removes this operation entirely, there is no replacement | N/A | N/A |

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | rawResponse | boolean | false | Whether to return the raw XML response instead of the parsed result | | bodyIdentity | string|Object | - | Operator identity for this call (multi-operator authentication); overrides the configured value, null suppresses it |

⚠️ getDdsByIdentifiers is restricted by operator class. The V3 WSDL states the operation is available only to non-SME operators (standard operators and authorised representatives) — SME downstream operators have no access to it. A rejected call comes back as a NotFoundException (error.notFound === true, HTTP 404) with the message "You are not authorized to view this DDS.", deliberately indistinguishable from a genuinely missing record. Use getDds/getDdsByInternalReference for your own declarations — those are owner-scoped and not subject to this restriction.

Note: unlike V1/V2, decodeGeojson auto-decoding is not yet implemented for V3 — geometryGeojson in getDdsByIdentifiers results comes back base64-encoded exactly as received from the server.

Key Features

  • ✅ Unified backend: retrieval and submission are the same DDS V3 service under the hood
  • ✅ Batch retrieval: getDds accepts up to 100 UUIDs per call, same as V1/V2 getDdsInfo
  • ✅ Full statement retrieval: getDdsByIdentifiers returns the complete DDS statement, not just an overview
  • ✅ Consistent array fields: ddsInfo is always an array (even for a single overview result); commodities/producers/speciesInfo/groupedDeclarations inside a full statement are always arrays
  • ⚠️ No supply chain traversal: V2's getReferencedDds() has no V3 equivalent — grouping (groupedDeclarations) is a different concept, not a drop-in replacement

Detailed Method Reference

getDds(uuids, options)

// Single UUID
const ddsInfo = await retrievalV3.getDds('550e8400-e29b-41d4-a716-446655440000');

// Multiple UUIDs (max 100 per call)
const multipleDds = await retrievalV3.getDds([
  '550e8400-e29b-41d4-a716-446655440000',
  '6ba7b810-9dad-11d1-80b4-00c04fd430c8'
]);

// Returns:
// {
//   httpStatus: 200,
//   status: 200,
//   ddsInfo: [
//     {
//       uuid: 'uuid-string',
//       internalReferenceNumber: '26BEDWNW9JD1TN',
//       referenceNumber: '26BE7XTVCZAQ2S',
//       verificationNumber: 'SFFCB4Y3',
//       status: 'AVAILABLE',       // EudrStatusType
//       rejectionReason: null,
//       communicationToOperator: null,
//       date: '2026-05-20T09:55:01.000Z',
//       updatedBy: 'User3 User3',
//       version: '1'
//     }
//   ],
//   raw: 'xml-response',   // if rawResponse: true
//   parsed: { /* parsed XML object */ }
// }

getDdsByInternalReference(internalReferenceNumber, options)

const ddsList = await retrievalV3.getDdsByInternalReference('26BEDWNW9JD1TN');

// Returns: same ddsInfo overview shape as getDds

getDdsByIdentifiers(referenceNumber, verificationNumber, options)

const fullDds = await retrievalV3.getDdsByIdentifiers('26BE7XTVCZAQ2S', 'SFFCB4Y3');

// Returns:
// {
//   httpStatus: 200,
//   status: 200,
//   statement: {
//     activityType: 'IMPORT',
//     commodities: [{
//       position: '1',
//       descriptors: { descriptionOfGoods: '...', goodsMeasure: { netWeight: '300.000000', ... } },
//       hsHeading: '4410',
//       speciesInfo: [{ scientificName: '...', commonName: '...' }],
//       producers: [{ country: 'FR', geometryGeojson: 'BASE64_ENCODED_GEOJSON' }]
//     }],
//     geoLocationConfidential: 'false'
//     // ... rest of the DDS statement
//   }
// }

Error Handling

try {
  const result = await retrievalV3.getDds('some-uuid');
  console.log('Success:', result.ddsInfo);
} catch (error) {
  if (error.notFound) {
    // NotFoundException - no declaration matches the identifiers.
    // Declared as an explicit fault on all V3 get* operations since Information System release 8.2.1.
    console.error('Not found:', error.eudrErrorMessage); // error.httpStatus === 404, error.eudrErrorCode === 'EUDR_NOT_FOUND'
  } else if (error.eudrErrorCode) {
    console.error('EUDR error code:', error.eudrErrorCode);
  } else {
    // Inspect the raw SOAP fault for BusinessRulesValidationException details
    console.error(error.details?.soapFault);
  }
}

Configuration Examples

// Production environment with SSL validation
const productionRetrievalV3Client = new EudrRetrievalClientV3({
  username: process.env.EUDR_USERNAME,
  password: process.env.EUDR_PASSWORD,
  webServiceClientId: 'eudr-repository',
  ssl: true,
  timeout: 30000
});

// Development environment with relaxed SSL
const devRetrievalV3Client = new EudrRetrievalClientV3({
  username: process.env.EUDR_USERNAME,
  password: process.env.EUDR_PASSWORD,
  webServiceClientId: 'eudr-test',
  ssl: false,
  timeout: 10000
});

// Manual endpoint override
const customRetrievalV3Client = new EudrRetrievalClientV3({
  endpoint: 'https://custom-endpoint.com/ws/EUDRDueDiligenceStatementServiceV3',
  username: 'user',
  password: 'pass',
  webServiceClientId: 'custom-client',
  ssl: false
});

🌱 EudrSimplifiedDeclarationClientV3 (V3)

Single unified client for the new Simplified Declaration (SD) V3 service — see V3 Simplified Declaration Client for when to use SD instead of DDS.

Methods

| Method | Description | Parameters | Returns | |--------|-------------|------------|---------| | submitSd(request, options) | Submit a new Simplified Declaration | request (Object), options (Object) | Promise with sdIdentifier | | updateSd(sdIdentifier, statement, options) | Update an existing SD | sdIdentifier (String), statement (Object), options (Object) | Promise with uuid + lifecycle status | | withdrawSd(sdIdentifier, options) | Withdraw an SD | sdIdentifier (String), options (Object) | Promise with uuid + lifecycle status | | getSd(uuids, options) | Retrieve SD overview by UUID(s)/version, max 100 | uuids (String, {uuid, version}, or Array), options (Object) | Promise with sdInfo array | | getSdByInternalReference(internalReferenceNumber, options) | Retrieve SD overview by internal reference | internalReferenceNumber (String), options (Object) | Promise with sdInfo array | | getSdByIdentifiers(referenceNumber, verificationNumber, options) | Retrieve full SD content | referenceNumber (String), verificationNumber (String), options (Object) | Promise with full statement |

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | rawResponse | boolean | false | Whether to return the raw XML response instead of the parsed result | | bodyIdentity | string|Object | - | Operator identity for this call (multi-operator authentication); overrides the configured value, null suppresses it |

Key Features

  • ✅ New V3-only concept: no V1/V2 precedent, no legacy field name compatibility concerns
  • ✅ Single unified client: write and retrieval operations in one class (unlike the DDS submission/retrieval split)
  • ✅ One-time declaration model: submitSd is meant to be called once per operator, not per shipment
  • ✅ Flexible producer location: geometryGeojson, postalAddress, or cadastralIdentifier (DDS requires GeoJSON only)
  • ⚠️ Asymmetric identifier field naming: submitSd returns sdIdentifier; updateSd/withdrawSd return uuid for the same value — this mirrors the actual WSDL, not an inconsistency in this library

Detailed Method Reference

submitSd(request, options)

const result = await sdClient.submitSd({
  operatorRole: 'MICRO_OPERATOR',  // MICRO_OPERATOR | REPRESENTATIVE_MSPO | MEMBER_STATE
  statement: {
    internalReferenceNumber: 'SD-REF-001',  // mandatory for SD (optional for DDS)
    activityType: 'IMPORT',  // DOMESTIC | IMPORT | EXPORT
    commodities: [{
      descriptors: { descriptionOfGoods: 'Cocoa beans', goodsMeasure: { netWeight: 1000 } },
      hsHeading: '1801',
      producers: [{
        producerCountry: 'CI',
        producerName: 'Producer Name',
        producerLocation: { geometryGeojson: 'BASE64_ENCODED_GEOJSON' }  // exactly one choice
      }]
    }],
    geoLocationConfidential: false
  }
});

// Returns: { httpStatus: 200, status: 200, sdIdentifier: 'uuid-string' }

updateSd(sdIdentifier, statement, options)

const result = await sdClient.updateSd('existing-sd-uuid', {
  internalReferenceNumber: 'SD-REF-001',
  activityType: 'IMPORT',
  commodities: [ /* ... */ ],
  geoLocationConfidential: false
});

// Returns: { httpStatus: 200, status: 'AVAILABLE', uuid: 'existing-sd-uuid', version: '2' }
// Note: response field is `uuid`, not `sdIdentifier` (see WSDL asymmetry note above).

withdrawSd(sdIdentifier, options)

const result = await sdClient.withdrawSd('existing-sd-uuid');
// Returns: { httpStatus: 200, status: 'WITHDRAWN', uuid: 'existing-sd-uuid' }

getSd(uuids, options)

// Plain uuid, or with an explicit version
await sdClient.getSd('existing-sd-uuid');
await sdClient.getSd({ uuid: 'existing-sd-uuid', version: 2 });
await sdClient.getSd(['uuid-1', { uuid: 'uuid-2', version: 1 }]); // up to 100 entries

// Returns: { httpStatus: 200, status: 200, sdInfo: [ { uuid, internalReferenceNumber, referenceNumber, verificationNumber, status, date, updatedBy, version, ... } ] }

getSdByInternalReference(internalReferenceNumber, options)

const sdList = await sdClient.getSdByInternalReference('SD-REF-001');
// Returns: same sdInfo overview shape as getSd

getSdByIdentifiers(referenceNumber, verificationNumber, options)

const fullSd = await sdClient.getSdByIdentifiers('S26BECB39D2GRX', 'H6ORMNTX');

// Returns:
// {
//   httpStatus: 200,
//   status: 200,
//   statement: {
//     activityType: 'IMPORT',
//     commodities: [{ descriptors: { descriptionOfGoods: '...' }, hsHeading: '1801', producers: [{ producerCountry: 'CI' }] }],
//     geoLocationConfidential: 'false'
//     // Note: no speciesInfo field on SD commodities (unlike DDS)
//   }
// }

Error Handling

try {
  await sdClient.submitSd({ operatorRole: 'OPERATOR', statement: { /* ... */ } });
} catch (error) {
  if (error.eudrErrorCode === 'EUDR_V3_SD_OPERATOR_ROLE_INVALID') {
    console.error('Invalid SD operatorRole:', error.message);
  } else if (error.eudrErrorCode === 'EUDR_V3_SD_PRODUCER_LOCATION_INVALID') {
    console.error('Producer location must be exactly one of geometryGeojson/postalAddress/cadastralIdentifier:', error.message);
  } else if (error.details?.soapFault) {
    console.error('SOAP fault:', error.details.soapFault.faultString);
  }
}

See the SD validation errors table above for the full list of EUDR_V3_SD_* codes.

Configuration Examples

// Production environment with SSL validation
const productionSdClient = new EudrSimplifiedDeclarationClientV3({
  username: process.env.EUDR_USERNAME,
  password: process.env.EUDR_PASSWORD,
  webServiceClientId: 'eudr-repository',
  ssl: true,
  timeout: 30000
});

// Development environment with relaxed SSL
const devSdClient = new EudrSimplifiedDeclarationClientV3({
  username: process.env.EUDR_USERNAME,
  password: process.env.EUDR_PASSWORD,
  webServiceClientId: 'eudr-test',
  ssl: false,
  timeout: 10000
});

// Manual endpoint override
const customSdClient = new EudrSimplifiedDeclarationClientV3({
  endpoint: 'https://custom-endpoint.com/ws/EUDRSimplifiedDeclarationServiceV3',
  username: 'user',
  password: 'pass',
  webServiceClientId: 'custom-client',
  ssl: false
});

🔍 EudrVerifyDeclarationClientV3 (V3)

Client for the EUDRVerifyDeclarationServiceV3 service — lets any party in the supply chain (not just the submitting operator) confirm that a DDS or SD declaration is authentic and in a usable status, given only its reference number and verification number. Specified in "EUDR Downstream Operator and Trader API Reference v1.0" §4.1 (not the main Operator API Reference, which only lists this service in its summary table).

Methods

| Method | Description | Parameters | Returns | |--------|-------------|------------|---------| | verifyDeclaration(referenceNumber, verificationNumber, options) | Verify a DDS or SD by reference + verification number | referenceNumber (String), verificationNumber (String), options (Object) | Promise with result, status, dateTime |

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | rawResponse | boolean | false | Whether to return the raw XML response instead of the parsed result |

Key Features

  • ✅ Single-operation client: only verifyDeclaration, no submission/retrieval split
  • ✅ Role-agnostic: usable by Operators, Authorised Representatives, and SME/Non-SME Downstream Operators or Traders alike — unlike DDS submission, which is operator-only
  • ✅ Works for both DDS and SD: the same referenceNumber/verificationNumber pair works regardless of which service originally issued the declaration
  • ⚠️ No bodyIdentity support: the Verify Declaration WSDL does not declare the BodyIdentity header, so multi-operator authentication does not apply to this client
  • ✅ Three-way result: EXISTING_USABLE, EXISTING_NON_USABLE, or NON_EXISTENT — status (the underlying EudrStatusType) is only present for the two EXISTING_* outcomes

Wording: the TRACES NT web interface labels these three outcomes VALID, NOT VALID and NOT FOUND. The API values are unchanged: EXISTING_USABLE → VALID, EXISTING_NON_USABLE → NOT VALID, NON_EXISTENT → NOT FOUND.

To read the full content of a declaration rather than just verify it, use getDdsByIdentifiers / getSdByIdentifiers with the same reference + verification number pair. That operation is restricted to non-SME operators — SME downstream operators and traders can verify existence but cannot read content.

Detailed Method Reference

verifyDeclaration(referenceNumber, verificationNumber, options)

// Declaration exists and is in a usable status (e.g. AVAILABLE)
const usable = await verifyDeclarationV3Client.verifyDeclaration('EUDR00000001BE', 'VN-2025-ABC12');
// Returns: { httpStatus: 200, result: 'EXISTING_USABLE', status: 'AVAILABLE', dateTime: '2026-05-20T10:00:00.000Z' }

// Declaration exists but is not in a usable status (e.g. WITHDRAWN, REJECTED, SUSPENDED)
const nonUsable = await verifyDeclarationV3Client.verifyDeclaration('EUDR00000002BE', 'VN-2025-XYZ99');
// Returns: { httpStatus: 200, result: 'EXISTING_NON_USABLE', status: 'WITHDRAWN', dateTime: '2026-05-20T10:00:00.000Z' }

// No declaration matches the reference/verification number combination
const missing = await verifyDeclarationV3Client.verifyDeclaration('EUDR99999999BE', 'VN-0000-NOPE1');
// Returns: { httpStatus: 200, result: 'NON_EXISTENT', status: null, dateTime: '2026-05-20T10:00:00.000Z' }

Error Handling

try {
  await verifyDeclarationV3Client.verifyDeclaration('BAD-REF', 'BAD-VN');
} catch (error) {
  if (error.eudrErrors?.length > 0) {
    // BusinessRulesValidationException for this service reports { field, message } without an error code
    error.eudrErrors.forEach(e => console.error(`${e.field}: ${e.message}`));
  } else if (error.httpStatus === 403) {
    console.error('Permission denied - role not authorized to verify declarations:', error.message);
  } else if (error.details?.soapFault) {
    console.error('SOAP fault:', error.details.soapFault.faultString);
  }
}

Configuration Examples

// Production environment with SSL validation
const productionVerifyClient = new EudrVerifyDeclarationClientV3({
  username: process.env.EUDR_USERNAME,
  password: process.env.EUDR_PASSWORD,
  webServiceClientId: 'eudr-repository',
  ssl: true,
  timeout: 30000
});

// Development environment with relaxed SSL
const devVerifyClient = new EudrVerifyDeclarationClientV3({
  username: process.env.EUDR_USERNAME,
  password: process.env.EUDR_PASSWORD,
  webServiceClientId: 'eudr-test',
  ssl: false,
  timeout: 10000
});

// Manual endpoint override
const customVerifyClient = new EudrVerifyDeclarationClientV3({
  endpoint: 'https://custom-endpoint.com/ws/EUDRVerifyDeclarationServiceV3',
  username: 'user',
  password: 'pass',
  webServiceClientId: 'custom-client',
  ssl: false
});

🚀 Flexible Array Fields

All V3 statement fields that the schema allows to repeat can be provided as either a single object or an array of objects — this library normalizes either input shape before building the SOAP request, and always normalizes them back to arrays in retrieval responses.

Supported Flexible Fields (V3)

| Field | Where | Description | |-------|-------|-------------| | commodities | DDS & SD statement | Commodity entries | | producers | DDS & SD commodity | Producer entries | | speciesInfo | DDS commodity only (not on SD) | Species entries | | groupedDeclarations | DDS & SD statement | Referenced declarations for grouping | | postalAddress | SD producer location | Alternative postal address(es) for a production location | | cadastralIdentifier | SD producer location | Alternative land-registry identifier(s) | | operatorReferenceNumber | representedOperator (DDS & SD) | Operator identifiers, up to 12 |

Schema change vs V1/V2: representedOperator.operatorReferenceNumber is a structured { identifierType, identifierValue } object in V3, where V1/V2 used operator.referenceNumber. It may repeat up to 12 times (pass an array); a single object is still accepted and is the common case. identifierType is validated against the V3 enum — the V1/V2 IMO-based values (ship_man_comp_imo, ship_reg_owner_imo) and remos no longer exist and are rejected with EUDR_V3_IDENTIFIER_TYPE_INVALID.

Examples

Single object (works the same as an array of one):

const request = {
  operatorRole: 'OPERATOR',
  statement: {
    activityType: 'IMPORT',
    commodities: {  // single commodity object, not wrapped in []
      descriptors: { descriptionOfGoods: 'Wood', goodsMeasure: { netWeight: 100 } },
      hsHeading: '4401',
      speciesInfo: { scientificName: 'Fagus sylvatica', commonName: 'European Beech' }, // single object
      producers: { country: 'HR', name: 'Forest Company', geometryGeojson: 'BASE64_ENCODED_GEOJSON' } // single object
    },
    geoLocationConfidential: false
  }
};

Array format (multiple items):

const request = {
  operatorRole: 'OPERATOR',
  statement: {
    activityType: 'IMPORT',
    commodities: [{
      descriptors: { descriptionOfGoods: 'Wood', goodsMeasure: { netWeight: 100 } },
      hsHeading: '4401',
      speciesInfo: [
        { scientificName: 'Fagus sylvatica', commonName: 'European Beech' },
        { scientificName: 'Quercus robur', commonName: 'English Oak' }
      ],
      producers: [
        { country: 'HR', name: 'Croatian Forest Company', geometryGeojson: 'BASE64_ENCODED_GEOJSON' },
        { country: 'DE', name: 'German Wood Supplier', geometryGeojson: 'BASE64_ENCODED_GEOJSON' }
      ]
    }],
    geoLocationConfidential: false,
    groupedDeclarations: [  // array of grouped declarations
      { groupedDeclaration: '25NLSN6LX69730' },
      { groupedDeclaration: '25NLWPAZWQ8865' }
    ]
  }
};

Mixed usage (maximum flexibility):

const request = {
  operatorRole: