DEV Community

Abdullatif Badr
Abdullatif Badr

Posted on

I built a modern PHP client for the EU Deforestation Regulation API

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
Enter fullscreen mode Exit fullscreen mode

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),
    ],
);
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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";
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)