If you work in supply chain, logistics, or import/export in the EU, you've probably heard about the EU Deforestation Regulation (EUDR).
It's a new compliance rule that requires companies importing commodities like wood, coffee, cocoa, palm oil, soy, cattle, and rubber into the EU to submit due diligence statements proving the products aren't linked to deforestation.
The EU provides a SOAP API for this. The API is complex. The documentation doesn't help much.
I spent some time building a PHP client that provides a clean, modern interface over the SOAP layer.
What is EUDR?
The EU Deforestation Regulation went into effect in 2023. It requires companies importing certain commodities into the EU to prove those products aren't linked to deforestation or forest degradation.
You need to submit a "due diligence statement" (DDS) before your goods can enter the EU. This includes geolocation data for where the products were sourced, operator information, commodity details, and more.
The EU provides a SOAP API to submit and query this data. That's where the problems start.
The API problems
Problem 1: SOAP complexity
The EUDR API is a SOAP service. If you've worked with SOAP before, you know it's verbose and harder to work with than modern REST APIs. XML envelopes, namespaces, security headers, fault handling.
It's not impossible, but it's not pleasant either.
Problem 2: Poor documentation
The official EUDR API docs give you the spec but don't show you how to actually use it well. The examples are either missing or incomplete. Error messages aren't always clear about what went wrong.
You'll spend time guessing what the API expects and debugging XML structures.
Problem 3: Complex data structures
A single DDS can have nested data: operators, commodities, producers, species info, geolocation data, associated statements. Building this manually in arrays or stdClass objects gets messy fast.
The solution: EUDR PHP Client
I built a PHP client that solves these problems.
Clean fluent API
Instead of building arrays or XML manually, you use immutable builders:
use Eudr\Requests\V2\SubmitDdsRequest;
use Eudr\Data\Commodity;
use Eudr\Data\Producer;
use Eudr\Data\SpeciesInfo;
use Eudr\Enums\OperatorType;
use Eudr\Enums\ActivityType;
$request = SubmitDdsRequest::make()
->withOperatorType(OperatorType::OPERATOR)
->withActivityType(ActivityType::IMPORT)
->withInternalReference('MY-REF-2024-001')
->withCountryOfActivity('DE')
->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
All request objects are immutable. Each with* or add* method returns a new instance. No side effects.
PSR standards
The client uses PSR-18 for HTTP (auto-discovers Guzzle, Symfony HttpClient, or any PSR-18 implementation), PSR-17 for request/response factories, and PSR-3 for logging if you provide a logger.
You can inject your own HTTP client and factories if needed.
Middleware pipeline
The client supports middleware for retry logic, logging, or custom behavior:
use Eudr\Http\Middleware\RetryMiddleware;
$client = new EudrClient(
config: $config,
middleware: [
new RetryMiddleware(maxAttempts: 3, baseDelayMs: 100),
],
);
Retry middleware handles 5xx responses and network failures with exponential backoff. Logging middleware is automatically enabled if you provide a PSR-3 logger in the config.
You can write custom middleware by implementing the Middleware interface.
Comprehensive error handling
All exceptions extend EudrException. API faults are parsed into structured ErrorResponse objects:
use Eudr\Exceptions\ApiException;
use Eudr\Exceptions\AuthenticationException;
use Eudr\Exceptions\ValidationException;
try {
$response = $client->dds()->submit($request);
} catch (ValidationException $e) {
// Missing required fields or invalid data
} catch (AuthenticationException $e) {
// Check your credentials
} catch (ApiException $e) {
// SOAP fault from the API
foreach ($e->error->errors as $detail) {
echo "{$detail->id}: {$detail->message} (field: {$detail->field})\n";
}
}
The ErrorResponse object parses the TracesNT error namespace and gives you structured error details with ID, message, and field.
PHPStan level 9
The entire codebase is strictly typed and passes PHPStan level 9. No mixed types, no magic. Everything is explicit.
Supported operations
The client supports both V1 and V2 of the EUDR API. V2 is the current recommended version.
| Operation | Description |
|---|---|
| Submit | Create a new DDS |
| Amend | Modify an existing DDS |
| Retract | Cancel/withdraw a DDS |
| Retrieve | Get DDS info by UUID |
| RetrieveMany | Batch retrieve up to 100 UUIDs |
| RetrieveByReference | Get DDS by internal reference number |
| GetStatementByIdentifiers | Cross-supply-chain retrieval |
| GetReferencedDds | Follow referenced DDS chain (V2 only) |
| Echo | Test connectivity and authentication |
Example: Batch retrieval
$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";
}
Example: Cross-supply-chain retrieval
Retrieve a supplier's DDS using their shared reference and verification numbers:
$response = $client->dds()->getStatementByIdentifiers(
referenceNumber: '24FRIOBORU2228',
verificationNumber: 'LWKAOH97'
);
echo $response->operatorName; // "FR DDS OPER TRAD AUTH REP"
echo $response->operatorCountry; // "FR"
echo $response->status->value; // "AVAILABLE"
Framework support
It's framework-agnostic. Works with:
- Laravel
- Symfony
- Standalone PHP projects
PSR-4 compliant, so it integrates cleanly with any modern PHP setup.
Why I built this
I work on backend systems for e-commerce and logistics. One of the projects I was involved with needed EUDR compliance for importing goods into the EU.
The API is complex. The documentation doesn't give you much beyond the spec. I didn't want to spend days building and debugging SOAP requests manually.
So I built a client, tested it against the sandbox and production environments, documented it properly, and open-sourced it.
If you're building EUDR compliance tools, this might save you some time.
What's next
The client covers all the main API operations. If there are edge cases or additional features that would be useful, open an issue or submit a PR.
MIT licensed. Use it, fork it, modify it.
Link: github.com/4bdullatif/eudr-php-client
If you're dealing with EUDR compliance and this helps, star the repo or share it with your team.
Top comments (0)