4bdullatif / eudr-php-client
Framework-agnostic PHP client for the EU Deforestation Regulation (EUDR) API with WS-Security authentication
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/4bdullatif/eudr-php-client
Requires
- php: ^8.3
- ext-dom: *
- ext-libxml: *
- ext-mbstring: *
- ext-simplexml: *
- php-http/discovery: ^1.19
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^2.0
- psr/log: ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- guzzlehttp/guzzle: ^7.0
- guzzlehttp/psr7: ^2.0
- php-http/mock-client: ^1.6
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
Suggests
- guzzlehttp/guzzle: PSR-18 HTTP client implementation
- symfony/http-client: PSR-18 HTTP client implementation (with psr18 adapter)
This package is auto-updated.
Last update: 2026-02-05 16:10:29 UTC
README
A framework-agnostic PHP client for the EU Deforestation Regulation (EUDR) TracesNT SOAP API. Supports both V1 and V2 endpoints with full WS-Security authentication.
Features
- Full V1 and V2 API coverage: Submit, Amend, Retract, Retrieve, and cross-supply-chain operations
- WS-Security password digest authentication
- Immutable request builders with fluent API
- PSR-18 HTTP client with auto-discovery
- Configurable middleware pipeline (retry, logging, custom)
- Strict types and PHPStan level 9 throughout
Table of Contents
- Requirements
- Installation
- Quick Start
- Configuration
- Supported Operations
- Usage
- Middleware
- Error Handling
- API Versions
- Development
- Architecture
- License
Requirements
- PHP 8.3 or higher
ext-dom,ext-libxml,ext-mbstring, andext-simplexml- A PSR-18 HTTP client (e.g. Guzzle, Symfony HttpClient)
- PSR-17 HTTP factories
Installation
composer require 4bdullatif/eudr-php-client
If you don't already have a PSR-18 HTTP client:
composer require guzzlehttp/guzzle guzzlehttp/psr7
Quick Start
use Eudr\Config\Config; use Eudr\Config\Credentials; use Eudr\Data\Commodity; use Eudr\Data\Producer; use Eudr\Data\SpeciesInfo; use Eudr\Enums\ActivityType; use Eudr\Enums\OperatorType; use Eudr\EudrClient; use Eudr\Requests\V2\SubmitDdsRequest; $client = new EudrClient( config: new Config( baseUrl: 'https://webgate.acceptance.ec.europa.eu/tracesnt/ws', credentials: new Credentials( username: 'your-username', authKey: 'your-auth-key', clientId: 'your-client-id', ), ), ); $request = SubmitDdsRequest::make() ->withOperatorType(OperatorType::OPERATOR) ->withActivityType(ActivityType::IMPORT) ->withInternalReference('MY-REF-2024-001') ->addCommodity( Commodity::make() ->position(1) ->description('Tropical hardwood lumber') ->hsHeading('440399') ->netWeight(5000.0) ->addSpeciesInfo(new SpeciesInfo('Swietenia macrophylla', 'Mahogany')) ->addProducer(new Producer('BR', base64_encode('{"type":"Point","coordinates":[-47.87,-15.79]}'))) ->build(), ); $response = $client->dds()->submit($request); echo $response->ddsIdentifier; // UUID of the created DDS
Configuration
Direct construction
use Eudr\Config\Config; use Eudr\Config\Credentials; $config = new Config( baseUrl: 'https://webgate.acceptance.ec.europa.eu/tracesnt/ws', credentials: new Credentials( username: 'your-username', authKey: 'your-auth-key', clientId: 'your-client-id', ), timeout: 30, // HTTP timeout in seconds (default: 30) validateRequests: true, // Validate before sending (default: true) logger: $psrLogger, // Optional PSR-3 logger );
From array
$config = Config::fromArray([ 'baseUrl' => 'https://webgate.acceptance.ec.europa.eu/tracesnt/ws', 'username' => 'your-username', 'authKey' => 'your-auth-key', 'clientId' => 'your-client-id', 'timeout' => 30, 'validateRequests' => true, ]);
Providing your own HTTP client
PSR-18/PSR-17 implementations are auto-discovered via php-http/discovery. You can also inject them explicitly:
$client = new EudrClient( config: $config, httpClient: $psrHttpClient, requestFactory: $psrRequestFactory, streamFactory: $psrStreamFactory, middleware: [ new RetryMiddleware(maxAttempts: 3, baseDelayMs: 200), ], );
Supported Operations
| Operation | Description | V1 | V2 |
|---|---|---|---|
| Submit | Create a new DDS | Y | Y |
| Amend | Modify an existing DDS | Y | Y |
| Retract | Cancel/withdraw a DDS | Y | Y |
| Retrieve | Get DDS info by UUID | Y | Y |
| RetrieveMany | Batch retrieve up to 100 UUIDs | Y | Y |
| RetrieveByReference | Get DDS by internal reference number | Y | Y |
| GetStatementByIdentifiers | Cross-supply-chain retrieval | Y | Y |
| GetReferencedDds | Follow referenced DDS chain | - | Y |
| Echo | Test connectivity and authentication | Y | Y |
Usage
Submitting a DDS
All request objects are immutable. Each with*/add* method returns a new instance.
use Eudr\Data\Address; use Eudr\Data\Commodity; use Eudr\Data\EconomicOperator; use Eudr\Data\Producer; use Eudr\Data\SpeciesInfo; use Eudr\Enums\ActivityType; use Eudr\Enums\OperatorType; use Eudr\Requests\V2\SubmitDdsRequest; $request = SubmitDdsRequest::make() ->withOperatorType(OperatorType::OPERATOR) ->withActivityType(ActivityType::IMPORT) ->withInternalReference('MY-REF-2024-001') ->withCountryOfActivity('DE') ->withBorderCrossCountry('NL') ->withComment('Annual timber import') ->withGeoLocationConfidential() ->withOperator(new EconomicOperator( name: 'Example GmbH', address: new Address( street: 'Hauptstrasse', number: '42', postcode: '10115', city: 'Berlin', countryCode: 'DE', ), email: 'contact@example.com', phone: '+49 30 1234567', referenceNumbers: [ ['identifierType' => 'EORI', 'identifierValue' => 'DE123456789'], ], )) ->addCommodity( Commodity::make() ->position(1) ->description('Tropical hardwood lumber') ->hsHeading('440399') ->volume(200.0) ->netWeight(5000.0) ->numberOfUnits(100) ->percentageEstimationOrDeviation(2.5) ->supplementaryUnit('m3') ->supplementaryUnitQualifier('CBM') ->addSpeciesInfo(new SpeciesInfo('Swietenia macrophylla', 'Mahogany')) ->addProducer(new Producer( country: 'BR', geometryGeojson: base64_encode('{"type":"Point","coordinates":[-47.87,-15.79]}'), name: 'Brazilian Forest Co', )) ->build(), ) ->addAssociatedStatement('REF-2024-001', 'VER-2024-001'); $response = $client->dds()->submit($request); $response->ddsIdentifier; // "3f09ab3f-4c97-4663-8463-89d58f1d646b" $response->isSuccess(); // true
Amending a DDS
Modify an existing DDS in AVAILABLE status. The activity type cannot be changed from the original.
use Eudr\Requests\V2\AmendDdsRequest; $request = AmendDdsRequest::make() ->withDdsIdentifier('3f09ab3f-4c97-4663-8463-89d58f1d646b') ->withOperatorType(OperatorType::OPERATOR) ->withActivityType(ActivityType::IMPORT) ->withInternalReference('MY-REF-2024-001-AMENDED') ->addCommodity($commodity); $response = $client->dds()->amend($request); $response->isSuccess(); // true (status === 'SC_200_OK')
Retracting a DDS
Cancel a DDS in SUBMITTED status or withdraw one in AVAILABLE status.
use Eudr\Requests\V2\RetractDdsRequest; $request = RetractDdsRequest::make() ->withDdsIdentifier('3f09ab3f-4c97-4663-8463-89d58f1d646b'); $response = $client->dds()->retract($request); $response->isSuccess(); // true
Retrieving a DDS
Single retrieval
$response = $client->dds()->retrieve('3f09ab3f-4c97-4663-8463-89d58f1d646b'); $response->identifier; // UUID $response->internalReferenceNumber; // "MY-REF-001" $response->referenceNumber; // "24FRXVV3VOS991" $response->verificationNumber; // "SEKUYXPP" $response->status; // DdsStatus::AVAILABLE $response->rejectionReason; // null or string $response->communicationToOperatorDate; // null or CA communication date $response->communicationToOperatorMessage; // null or CA communication message
Batch retrieval (up to 100 UUIDs)
$responses = $client->dds()->retrieveMany([ '3f09ab3f-4c97-4663-8463-89d58f1d646b', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', ]); foreach ($responses as $response) { echo $response->identifier . ': ' . $response->status->value . "\n"; }
By internal reference number
// Returns the first match $response = $client->dds()->retrieveByReference('MY-REF-001'); // Returns all matches (up to 1000) $responses = $client->dds()->retrieveAllByReference('MY-REF-001');
Cross-Supply-Chain Retrieval
Get statement by identifiers
Retrieve a supplier's DDS using their shared reference and verification numbers:
$response = $client->dds()->getStatementByIdentifiers( referenceNumber: '24FRIOBORU2228', verificationNumber: 'LWKAOH97', ); $response->referenceNumber; // "24FRIOBORU2228" $response->activityType; // "IMPORT" $response->status; // DdsStatus::AVAILABLE $response->statusDate; // "2024-09-23T11:05:00.000" $response->operatorName; // "FR DDS OPER TRAD AUTH REP" $response->operatorCountry; // "FR" $response->associatedStatements; // [['referenceNumber' => '...']]
Get referenced DDS (V2 only)
Follow the chain of referenced DDS documents without requiring the original verification number:
$response = $client->dds()->getReferencedDds( referenceNumber: '25FR6CWUOLKN59', referenceDdsVerificationNumber: 'encrypted-verification-string', );
Echo Service
Test connectivity and authentication. Available in acceptance/testing environments only.
$response = $client->echo('hello'); $response->result; // Echo response from server $response->isSuccess(); // true
Middleware
The client supports a PSR-7 middleware pipeline for cross-cutting concerns.
Retry
Retries failed requests on 5xx responses and transport exceptions with exponential backoff:
use Eudr\Http\Middleware\RetryMiddleware; $client = new EudrClient( config: $config, middleware: [ new RetryMiddleware(maxAttempts: 3, baseDelayMs: 100), ], );
Logging
Automatically enabled when a PSR-3 logger is provided in the config. Logs request method/URI and response status/duration.
$config = new Config( // ... logger: $monologLogger, );
Custom middleware
Implement the Middleware interface:
use Eudr\Http\Middleware\Middleware; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; class RateLimitMiddleware implements Middleware { public function process(RequestInterface $request, callable $next): ResponseInterface { $this->waitForRateLimit(); $response = $next($request); $this->updateRateLimit($response); return $response; } }
Error Handling
All exceptions extend Eudr\Exceptions\EudrException:
RuntimeException
└── EudrException
├── ConfigurationException // Invalid config
├── ValidationException // Request validation failed
├── XmlException // Response XML parsing failed
├── ApiException // SOAP fault from the API
└── HttpException // HTTP 4xx/5xx errors
└── AuthenticationException // HTTP 401/403
use Eudr\Exceptions\ApiException; use Eudr\Exceptions\AuthenticationException; use Eudr\Exceptions\HttpException; use Eudr\Exceptions\ValidationException; use Eudr\Exceptions\XmlException; try { $response = $client->dds()->submit($request); } catch (ValidationException $e) { // Missing required fields, invalid data formats } catch (AuthenticationException $e) { // HTTP 401/403 - check credentials $e->statusCode; $e->responseBody; } catch (ApiException $e) { // SOAP fault returned by the EUDR API } catch (HttpException $e) { // Other HTTP errors (5xx, network issues) $e->statusCode; $e->responseBody; } catch (XmlException $e) { // Response XML could not be parsed }
SOAP fault error details
The ErrorResponse object provides structured access to SOAP fault details from the TracesNT error namespace:
// ErrorResponse fields: $error->faultCode; // SOAP fault code $error->faultString; // Human-readable error message $error->detail; // Raw XML detail string $error->errors; // ErrorDetail[] - parsed structured errors // Each ErrorDetail contains: foreach ($error->errors as $detail) { $detail->id; // e.g. "EUDR-REFERENCE-NUMBER-INVALID" $detail->message; // e.g. "Has not allowed characters" $detail->field; // e.g. "Reference number" }
API Versions
The package supports both V1 and V2 of the EUDR API. V2 is the current recommended version.
// V2 (default) $client->dds()->submit($v2Request); $client->dds()->amend($v2AmendRequest); $client->dds()->retract($v2RetractRequest); $client->dds()->retrieve('uuid'); $client->dds()->retrieveMany(['uuid-1', 'uuid-2']); $client->dds()->retrieveByReference('MY-REF-001'); $client->dds()->retrieveAllByReference('MY-REF-001'); $client->dds()->getStatementByIdentifiers('REF-001', 'VER-001'); $client->dds()->getReferencedDds('REF-001', 'encrypted-verification'); // V1 $client->ddsV1()->submit($v1Request); $client->ddsV1()->amend($v1AmendRequest); $client->ddsV1()->retract($v1RetractRequest); $client->ddsV1()->retrieve('uuid'); // Echo (connectivity test) $client->echo('hello');
Key differences between V1 and V2
| Feature | V1 | V2 |
|---|---|---|
| Operator address | nameAndAddress (flat string) |
operatorAddress (structured fields) |
| GoodsMeasure | All fields except percentageEstimationOrDeviation |
All fields including percentageEstimationOrDeviation |
| Namespace prefix | v1/v11 |
v2/v21 |
| Endpoint suffix | *ServiceV1 |
*ServiceV2 |
| GetReferencedDds | Not available | Available |
Enums
OperatorType - OPERATOR, TRADER, REPRESENTATIVE_OPERATOR, REPRESENTATIVE_TRADER
ActivityType - DOMESTIC, TRADE, IMPORT, EXPORT
DdsStatus - PENDING_CREATION, AVAILABLE, SUBMITTED, REJECTED, RETRACTED, CANCELLED, WITHDRAWN, ARCHIVED, UNKNOWN
Unknown API status values gracefully fall back to
DdsStatus::UNKNOWN.
Development
composer install # Install dependencies composer test # Run tests composer analyse # Static analysis (PHPStan level 9) composer cs-check # Code style check composer cs-fix # Fix code style composer check # Run all checks
Architecture
src/
├── Config/ Configuration and credentials
├── Data/ Immutable value objects (Commodity, Producer, Address, etc.)
├── Enums/ OperatorType, ActivityType, DdsStatus
├── Exceptions/ Exception hierarchy
├── Http/ PSR-18 connector and middleware pipeline
│ └── Middleware/ Retry, logging, and custom middleware
├── Requests/ SOAP request builders
│ ├── Builders/ Fluent builders for Commodity, Producer, Operator
│ ├── V1/ V1-specific requests and XML traits
│ └── V2/ V2-specific requests and XML traits
├── Resources/ API resource classes (DDS operations)
├── Responses/ SOAP response parsers
│ ├── V1/ V1 response parsers
│ └── V2/ V2 response parsers
├── Support/ XML utilities, namespace constants, SOAP envelope parser
└── EudrClient.php Main entry point