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.
- 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
- Requirements
- Installation
- Quick Start
- Configuration
- Supported Operations
- Usage
- Middleware
- Error Handling
- API Versions
- Development
- Architecture
- License
- 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
composer require 4bdullatif/eudr-php-clientIf you don't already have a PSR-18 HTTP client:
composer require guzzlehttp/guzzle guzzlehttp/psr7use 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 DDSuse 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
);$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,
]);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),
],
);| 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 |
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(); // trueModify 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')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$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$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";
}// Returns the first match
$response = $client->dds()->retrieveByReference('MY-REF-001');
// Returns all matches (up to 1000)
$responses = $client->dds()->retrieveAllByReference('MY-REF-001');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' => '...']]Follow the chain of referenced DDS documents without requiring the original verification number:
$response = $client->dds()->getReferencedDds(
referenceNumber: '25FR6CWUOLKN59',
referenceDdsVerificationNumber: 'encrypted-verification-string',
);Test connectivity and authentication. Available in acceptance/testing environments only.
$response = $client->echo('hello');
$response->result; // Echo response from server
$response->isSuccess(); // trueThe client supports a PSR-7 middleware pipeline for cross-cutting concerns.
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),
],
);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,
);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;
}
}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
}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"
}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');| 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 |
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.
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 checkssrc/
├── 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