The EU's Ecodesign for Sustainable Products Regulation (ESPR) mandates Digital Product Passports (DPP) for most physical goods sold in Europe starting 2026–2030 (phased by category). If you run a WooCommerce store shipping to EU customers, your products will need machine-readable passports containing lifecycle data, materials, repairability scores, and recycling instructions — all accessible via a physical carrier (QR code, RFID, or datamatrix).
This article walks through how we built a self-hosted WooCommerce plugin to generate DPP-compliant structured data and printable QR codes — no third-party SaaS, no ongoing fees.
What Is a Digital Product Passport?
A DPP is a structured data record attached to a physical product containing:
- Product identity: GTIN/EAN, model, manufacturer, batch/serial
- Materials & substances: Bill of materials, hazardous substances (SVHC list)
- Repairability: Spare parts availability, repair manual links, disassembly score
- Environmental footprint: Carbon footprint (kg CO₂e), energy efficiency class
- End-of-life: Recycling instructions, collection points, recyclate content %
- Compliance documents: CE declaration, REACH compliance, test reports
The data must be accessible via a carrier (QR code printed on product/packaging) pointing to a URL that returns structured data in a machine-readable format (JSON-LD recommended by the EU).
1. The Data Model — WooCommerce Product Meta
We store DPP data as WooCommerce product meta, mapped to the EU DPP data model:
<?php
declare(strict_types=1);
class SP_DPP_Data_Model {
public const FIELDS = [
// Identity
'dpp_gtin' => 'sanitize_text_field',
'dpp_manufacturer_name' => 'sanitize_text_field',
'dpp_manufacturer_country' => 'sanitize_text_field',
'dpp_batch_number' => 'sanitize_text_field',
// Materials
'dpp_main_material' => 'sanitize_text_field',
'dpp_recycled_content_pct' => 'absint',
'dpp_hazardous_substances' => 'sanitize_textarea_field',
// Repairability
'dpp_repairability_score' => 'sanitize_text_field',
'dpp_spare_parts_url' => 'esc_url_raw',
'dpp_repair_manual_url' => 'esc_url_raw',
'dpp_warranty_years' => 'absint',
// Environmental
'dpp_carbon_footprint_kg' => 'sanitize_text_field',
'dpp_energy_class' => 'sanitize_text_field',
'dpp_energy_kwh_year' => 'sanitize_text_field',
// End of life
'dpp_recycling_instructions' => 'sanitize_textarea_field',
'dpp_disassembly_time_min' => 'absint',
'dpp_collection_point_url' => 'esc_url_raw',
// Compliance
'dpp_ce_declaration_url' => 'esc_url_raw',
'dpp_reach_compliant' => 'absint',
];
public function save_product_meta( int $product_id ): void {
if ( ! current_user_can( 'edit_product', $product_id ) ) return;
if ( ! isset( $_POST['sp_dpp_nonce'] ) || ! wp_verify_nonce(
sanitize_text_field( wp_unslash( $_POST['sp_dpp_nonce'] ) ),
'sp_dpp_save_' . $product_id
) ) return;
$product = wc_get_product( $product_id );
if ( ! $product ) return;
foreach ( self::FIELDS as $field => $sanitizer ) {
if ( isset( $_POST[ $field ] ) ) {
$value = call_user_func( $sanitizer, wp_unslash( $_POST[ $field ] ) );
$product->update_meta_data( '_' . $field, $value );
}
}
$product->save();
}
public function get_passport_data( int $product_id ): array {
$product = wc_get_product( $product_id );
if ( ! $product ) return [];
$data = [];
foreach ( array_keys( self::FIELDS ) as $field ) {
$data[ $field ] = $product->get_meta( '_' . $field );
}
return $data;
}
}
2. JSON-LD Passport Endpoint (WP REST API)
The QR code points to a public REST endpoint returning JSON-LD:
<?php
declare(strict_types=1);
class SP_DPP_REST_Controller extends WP_REST_Controller {
protected $namespace = 'sp-dpp/v1';
protected $rest_base = 'passport';
public function register_routes(): void {
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<product_id>[\d]+)', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_passport' ],
'permission_callback' => '__return_true', // Public
'args' => [
'product_id' => [
'validate_callback' => fn($v) => is_numeric($v),
'sanitize_callback' => 'absint',
],
],
],
] );
}
public function get_passport( WP_REST_Request $request ): WP_REST_Response {
$product_id = $request->get_param( 'product_id' );
$product = wc_get_product( $product_id );
if ( ! $product || ! $product->is_visible() ) {
return new WP_REST_Response( [ 'error' => 'Product not found' ], 404 );
}
$model = new SP_DPP_Data_Model();
$data = $model->get_passport_data( $product_id );
$passport = [
'@context' => [
'schema' => 'https://schema.org/',
'dpp' => 'https://digital-product-passport.eu/ns#',
],
'@type' => [ 'schema:Product', 'dpp:DigitalProductPassport' ],
'@id' => get_permalink( $product_id ),
'schema:name' => $product->get_name(),
'schema:gtin' => $data['dpp_gtin'],
'dpp:materials' => [
'dpp:primaryMaterial' => $data['dpp_main_material'],
'dpp:recycledContentPct' => (int) $data['dpp_recycled_content_pct'],
'dpp:hazardousSubstances' => json_decode( $data['dpp_hazardous_substances'] ?: '[]', true ),
],
'dpp:repairability' => [
'dpp:score' => $data['dpp_repairability_score'],
'dpp:sparePartsUrl' => $data['dpp_spare_parts_url'],
'dpp:repairManual' => $data['dpp_repair_manual_url'],
'dpp:warrantyYears' => (int) $data['dpp_warranty_years'],
],
'dpp:environmental' => [
'dpp:carbonFootprintKg' => (float) $data['dpp_carbon_footprint_kg'],
'dpp:energyClass' => $data['dpp_energy_class'],
'dpp:energyKwhYear' => (float) $data['dpp_energy_kwh_year'],
],
'dpp:endOfLife' => [
'dpp:recyclingInstructions' => $data['dpp_recycling_instructions'],
'dpp:disassemblyTimeMin' => (int) $data['dpp_disassembly_time_min'],
'dpp:collectionPointUrl' => $data['dpp_collection_point_url'],
],
'dpp:compliance' => [
'dpp:ceDeclarationUrl' => $data['dpp_ce_declaration_url'],
'dpp:reachCompliant' => (bool) $data['dpp_reach_compliant'],
],
'dpp:generatedAt' => gmdate( 'c' ),
];
$response = new WP_REST_Response( $passport, 200 );
$response->header( 'Content-Type', 'application/ld+json' );
$response->header( 'Cache-Control', 'public, max-age=3600' );
return $response;
}
}
Endpoint URL: https://yourstore.com/wp-json/sp-dpp/v1/passport/123
3. QR Code Generation (No External API)
<?php
declare(strict_types=1);
class SP_DPP_QR_Generator {
public function generate_qr( int $product_id, int $size = 300 ): string {
$passport_url = rest_url( 'sp-dpp/v1/passport/' . $product_id );
$cache_key = 'sp_dpp_qr_' . $product_id . '_' . $size;
$cached = get_transient( $cache_key );
if ( $cached ) return $cached;
require_once SP_DPP_PATH . 'vendor/phpqrcode/qrlib.php';
ob_start();
QRcode::png( $passport_url, false, QR_ECLEVEL_M, 10, 2 );
$raw_png = ob_get_clean();
$data_uri = 'data:image/png;base64,' . base64_encode( $raw_png );
set_transient( $cache_key, $data_uri, WEEK_IN_SECONDS );
return $data_uri;
}
public function save_qr_file( int $product_id ): string {
$upload_dir = wp_upload_dir();
$dpp_dir = trailingslashit( $upload_dir['basedir'] ) . 'sp-dpp-qr/';
if ( ! file_exists( $dpp_dir ) ) {
wp_mkdir_p( $dpp_dir );
file_put_contents( $dpp_dir . 'index.php', '<?php // Silence is golden.' );
}
$filename = 'dpp-qr-' . $product_id . '.png';
require_once SP_DPP_PATH . 'vendor/phpqrcode/qrlib.php';
QRcode::png( rest_url( 'sp-dpp/v1/passport/' . $product_id ), $dpp_dir . $filename, QR_ECLEVEL_M, 10, 2 );
return trailingslashit( $upload_dir['baseurl'] ) . 'sp-dpp-qr/' . $filename;
}
}
4. Admin Product Tab
<?php
add_filter( 'woocommerce_product_data_tabs', function( array $tabs ): array {
$tabs['sp_dpp'] = [
'label' => __( 'Digital Passport', 'sp-dpp' ),
'target' => 'sp_dpp_product_data',
'priority' => 85,
];
return $tabs;
} );
add_action( 'woocommerce_product_data_panels', function(): void {
global $post;
wp_nonce_field( 'sp_dpp_save_' . $post->ID, 'sp_dpp_nonce' );
echo '<div id="sp_dpp_product_data" class="panel woocommerce_options_panel">';
echo '<div class="options_group">';
woocommerce_wp_text_input([
'id' => 'dpp_gtin',
'label' => __( 'GTIN / EAN', 'sp-dpp' ),
'description' => __( 'Global Trade Item Number', 'sp-dpp' ),
'desc_tip' => true,
'value' => get_post_meta( $post->ID, '_dpp_gtin', true ),
]);
woocommerce_wp_text_input([
'id' => 'dpp_repairability_score',
'label' => __( 'Repairability Score (e.g. 7.5/10)', 'sp-dpp' ),
'value' => get_post_meta( $post->ID, '_dpp_repairability_score', true ),
]);
woocommerce_wp_text_input([
'id' => 'dpp_carbon_footprint_kg',
'label' => __( 'Carbon Footprint (kg CO₂e)', 'sp-dpp' ),
'value' => get_post_meta( $post->ID, '_dpp_carbon_footprint_kg', true ),
]);
echo '</div>';
// QR preview
$qr = new SP_DPP_QR_Generator();
$src = $qr->generate_qr( $post->ID, 200 );
echo '<div class="options_group">';
echo '<p><strong>' . esc_html__( 'DPP QR Code', 'sp-dpp' ) . '</strong></p>';
echo '<img src="' . esc_attr( $src ) . '" width="150" />';
echo '<p><a href="' . esc_url( rest_url( 'sp-dpp/v1/passport/' . $post->ID ) ) . '" target="_blank">'
. esc_html__( 'View Passport JSON-LD', 'sp-dpp' ) . '</a></p>';
echo '</div>';
echo '</div>';
} );
5. Bulk Import via WooCommerce CSV
<?php
add_filter( 'woocommerce_csv_product_import_mapping_default_columns', function( array $columns ): array {
return array_merge( $columns, [
'GTIN' => 'dpp_gtin',
'Manufacturer' => 'dpp_manufacturer_name',
'Carbon Footprint kg' => 'dpp_carbon_footprint_kg',
'Recycled Content %' => 'dpp_recycled_content_pct',
'Repairability Score' => 'dpp_repairability_score',
'Energy Class' => 'dpp_energy_class',
] );
} );
add_filter( 'woocommerce_product_import_pre_insert_product_object',
function( WC_Product $product, array $data ): WC_Product {
foreach ( array_keys( SP_DPP_Data_Model::FIELDS ) as $field ) {
if ( isset( $data[ $field ] ) ) {
$product->update_meta_data( '_' . $field, sanitize_text_field( $data[ $field ] ) );
}
}
return $product;
}, 10, 2 );
ESPR Rollout Timeline
| Year | Categories |
|---|---|
| 2026 | Batteries, textiles, electronics |
| 2027 | Furniture, steel, cement, chemicals |
| 2028–2030 | All remaining categories |
The DPP endpoint must stay accessible for the full product lifecycle — typically 10+ years after sale.
Why Self-Hosted?
DPP SaaS platforms (Circularise, Renoon, Fairly Made) charge €200–500/mo per brand. For most WooCommerce stores, a one-time plugin generating compliant JSON-LD and QR codes covers the full technical requirement without ongoing costs.
The full plugin is on CodeCanyon: EU Digital Product Passport for WooCommerce (ESPR)
Questions about the JSON-LD structure or ESPR data requirements? Drop them in the comments.
Top comments (0)