Table of Contents
Introduction
GDPR Article 20 grants users the right to data portability: the right to obtain a copy of their personal data in a structured, commonly used, machine-readable format. For WordPress site owners, this isn't optional—it's a legal requirement that applies to any site handling data from EU residents.
Many site administrators think WordPress's built-in privacy tools handle this automatically. The truth is more nuanced. WordPress provides the framework through the wp_register_personal_data_exporter function, but implementing it properly requires understanding how your plugins and custom code store personal data.
This guide walks through building comprehensive WordPress GDPR data export functionality. We'll cover how WordPress's exporter system works, how to register custom exporters for your plugins, how to handle complex data structures, how to test your implementation, and how to document your process for compliance audits.
Getting GDPR data export right is essential not just for legal compliance, but for maintaining user trust. When users can easily retrieve their data, it demonstrates transparency. When you make it difficult, it damages your reputation and violates user expectations.
WP HealthKit has audited hundreds of WordPress sites and identified common data export gaps. Let's ensure your implementation is complete and compliant.
Understanding WordPress Personal Data Export System
WordPress 4.9.6 introduced the Personal Data Export feature, designed specifically to support GDPR Article 20 requirements. It's built into the Tools → Export Personal Data admin panel.
The system works like this:
- User (or admin on their behalf) requests a data export
- WordPress identifies all personal data associated with that user
- Each plugin/theme with personal data registers an exporter function
- Exporters return data in a standardized format
- WordPress compiles exporters' output into a single file
- Admin can download or email the file to the user
The key insight is that WordPress doesn't automatically know what data your plugin stores. You must explicitly register exporters to tell WordPress about it.
The Exporter Registration Hook
<?php
// Register an exporter for your plugin
add_filter('wp_privacy_personal_data_exporters', function($exporters) {
$exporters['my-plugin-exporter'] = array(
'exporter_friendly_name' => __('My Plugin Data', 'my-plugin'),
'callback' => 'my_plugin_export_personal_data',
);
return $exporters;
});
// The exporter callback function
function my_plugin_export_personal_data($email, $page = 1) {
// Find user by email
$user = get_user_by('email', $email);
if (!$user) {
return array('data' => array(), 'done' => true);
}
// Collect personal data
$data = array(
'group_id' => 'my-plugin-data',
'group_label' => __('My Plugin Data', 'my-plugin'),
'item_id' => 'item-' . $user->ID,
'data' => array(
array(
'name' => __('User ID', 'my-plugin'),
'value' => $user->ID,
),
array(
'name' => __('Email', 'my-plugin'),
'value' => $user->user_email,
),
),
);
return array(
'data' => array($data),
'done' => true,
);
}
?>
WordPress calls your exporter function with two parameters:
$email: The email address of the user whose data is being exported$page: For large datasets, pagination support (page 1, 2, etc.)
Your exporter must return an array with:
data: Array of exported data in the proper formatdone: Boolean indicating if there's more data to export
Data Format Standard
Each data item in your export must follow this structure:
<?php
$data_item = array(
'group_id' => 'my-plugin-orders', // Unique identifier for grouping
'group_label' => 'Customer Orders', // Human-readable group name
'item_id' => 'order-12345', // Unique identifier for this item
'data' => array( // Array of name/value pairs
array('name' => 'Order ID', 'value' => '12345'),
array('name' => 'Order Date', 'value' => '2026-01-15'),
array('name' => 'Total Amount', 'value' => '$99.99'),
array('name' => 'Items', 'value' => 'Product A, Product B'),
),
);
?>
Building a Complete WordPress GDPR Data Exporter
Let's build exporters for a realistic e-commerce plugin that stores:
- Order information
- Customer addresses
- Payment history
- Product reviews
<?php
/**
* E-Commerce Plugin - GDPR Data Export
* Handles personal data export for customers
*/
class Ecommerce_GDPR_Export {
public function __construct() {
// Register all exporters on plugin load
add_filter('wp_privacy_personal_data_exporters', array($this, 'register_exporters'));
}
/**
* Register all data exporters
*/
public function register_exporters($exporters) {
$exporters['ecommerce_customer_data'] = array(
'exporter_friendly_name' => __('Customer Account Data', 'ecommerce'),
'callback' => array($this, 'export_customer_data'),
);
$exporters['ecommerce_orders'] = array(
'exporter_friendly_name' => __('Orders and Purchases', 'ecommerce'),
'callback' => array($this, 'export_order_data'),
);
$exporters['ecommerce_reviews'] = array(
'exporter_friendly_name' => __('Product Reviews', 'ecommerce'),
'callback' => array($this, 'export_review_data'),
);
$exporters['ecommerce_wishlist'] = array(
'exporter_friendly_name' => __('Wishlist', 'ecommerce'),
'callback' => array($this, 'export_wishlist_data'),
);
return $exporters;
}
/**
* Export customer account information
*/
public function export_customer_data($email, $page = 1) {
$user = get_user_by('email', $email);
if (!$user) {
return array('data' => array(), 'done' => true);
}
$data = array();
// Basic account data
$account_data = array(
'group_id' => 'ecommerce_account',
'group_label' => __('Account Information', 'ecommerce'),
'item_id' => 'account-' . $user->ID,
'data' => array(
array(
'name' => __('Username', 'ecommerce'),
'value' => $user->user_login,
),
array(
'name' => __('Email Address', 'ecommerce'),
'value' => $user->user_email,
),
array(
'name' => __('First Name', 'ecommerce'),
'value' => $user->first_name,
),
array(
'name' => __('Last Name', 'ecommerce'),
'value' => $user->last_name,
),
array(
'name' => __('Account Created', 'ecommerce'),
'value' => $user->user_registered,
),
array(
'name' => __('Last Login', 'ecommerce'),
'value' => get_user_meta($user->ID, 'last_login', true) ?: __('Never', 'ecommerce'),
),
),
);
$data[] = $account_data;
// Billing address
$billing_data = array(
'group_id' => 'ecommerce_billing',
'group_label' => __('Billing Address', 'ecommerce'),
'item_id' => 'billing-' . $user->ID,
'data' => array(
array(
'name' => __('Street Address', 'ecommerce'),
'value' => get_user_meta($user->ID, 'billing_address', true),
),
array(
'name' => __('City', 'ecommerce'),
'value' => get_user_meta($user->ID, 'billing_city', true),
),
array(
'name' => __('State/Province', 'ecommerce'),
'value' => get_user_meta($user->ID, 'billing_state', true),
),
array(
'name' => __('Postal Code', 'ecommerce'),
'value' => get_user_meta($user->ID, 'billing_zip', true),
),
array(
'name' => __('Country', 'ecommerce'),
'value' => get_user_meta($user->ID, 'billing_country', true),
),
array(
'name' => __('Phone', 'ecommerce'),
'value' => get_user_meta($user->ID, 'billing_phone', true),
),
),
);
$data[] = $billing_data;
return array('data' => $data, 'done' => true);
}
/**
* Export order history with pagination
*/
public function export_order_data($email, $page = 1) {
$user = get_user_by('email', $email);
if (!$user) {
return array('data' => array(), 'done' => true);
}
global $wpdb;
// Paginate orders (10 per page)
$items_per_page = 10;
$offset = ($page - 1) * $items_per_page;
// Assuming a custom orders table
$orders = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}ecommerce_orders
WHERE user_id = %d
ORDER BY created_at DESC
LIMIT %d OFFSET %d",
$user->ID,
$items_per_page,
$offset
)
);
$data = array();
foreach ($orders as $order) {
// Get order items
$items = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}ecommerce_order_items
WHERE order_id = %d",
$order->id
)
);
// Format item list
$item_list = array();
foreach ($items as $item) {
$item_list[] = wp_sprintf(
'%s (Qty: %d, $%s)',
$item->product_name,
$item->quantity,
number_format($item->price, 2)
);
}
$order_data = array(
'group_id' => 'ecommerce_orders',
'group_label' => __('Orders', 'ecommerce'),
'item_id' => 'order-' . $order->id,
'data' => array(
array(
'name' => __('Order Number', 'ecommerce'),
'value' => $order->order_number,
),
array(
'name' => __('Order Date', 'ecommerce'),
'value' => $order->created_at,
),
array(
'name' => __('Order Status', 'ecommerce'),
'value' => ucfirst($order->status),
),
array(
'name' => __('Order Total', 'ecommerce'),
'value' => '$' . number_format($order->total, 2),
),
array(
'name' => __('Items', 'ecommerce'),
'value' => implode('; ', $item_list),
),
array(
'name' => __('Shipping Address', 'ecommerce'),
'value' => wp_sprintf(
'%s, %s, %s %s',
$order->shipping_address,
$order->shipping_city,
$order->shipping_state,
$order->shipping_zip
),
),
array(
'name' => __('Payment Method', 'ecommerce'),
'value' => $order->payment_method,
),
),
);
$data[] = $order_data;
}
// Check if there are more orders
$total_orders = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}ecommerce_orders
WHERE user_id = %d",
$user->ID
)
);
$done = (($page * $items_per_page) >= $total_orders);
return array('data' => $data, 'done' => $done);
}
/**
* Export product reviews
*/
public function export_review_data($email, $page = 1) {
$user = get_user_by('email', $email);
if (!$user) {
return array('data' => array(), 'done' => true);
}
global $wpdb;
// Get user's comments (reviews)
$items_per_page = 10;
$offset = ($page - 1) * $items_per_page;
$comments = get_comments(array(
'user_id' => $user->ID,
'type' => 'product_review',
'number' => $items_per_page,
'offset' => $offset,
'status' => 'all', // Include all statuses for export
));
$data = array();
foreach ($comments as $comment) {
$review_data = array(
'group_id' => 'ecommerce_reviews',
'group_label' => __('Product Reviews', 'ecommerce'),
'item_id' => 'review-' . $comment->comment_ID,
'data' => array(
array(
'name' => __('Product', 'ecommerce'),
'value' => get_the_title($comment->comment_post_ID),
),
array(
'name' => __('Review Date', 'ecommerce'),
'value' => $comment->comment_date,
),
array(
'name' => __('Rating', 'ecommerce'),
'value' => get_comment_meta($comment->comment_ID, 'rating', true) . '/5',
),
array(
'name' => __('Review Title', 'ecommerce'),
'value' => get_comment_meta($comment->comment_ID, 'review_title', true),
),
array(
'name' => __('Review Text', 'ecommerce'),
'value' => $comment->comment_content,
),
array(
'name' => __('Status', 'ecommerce'),
'value' => ucfirst($comment->comment_approved),
),
),
);
$data[] = $review_data;
}
// Check for more reviews
$total_reviews = count(get_comments(array(
'user_id' => $user->ID,
'type' => 'product_review',
'count' => true,
)));
$done = (($page * $items_per_page) >= $total_reviews);
return array('data' => $data, 'done' => $done);
}
/**
* Export wishlist
*/
public function export_wishlist_data($email, $page = 1) {
$user = get_user_by('email', $email);
if (!$user) {
return array('data' => array(), 'done' => true);
}
// Get wishlist items from user meta
$wishlist = get_user_meta($user->ID, 'ecommerce_wishlist', true);
if (!is_array($wishlist) || empty($wishlist)) {
return array('data' => array(), 'done' => true);
}
$data = array();
foreach ($wishlist as $product_id) {
$product = get_post($product_id);
if (!$product) {
continue;
}
$item_data = array(
'group_id' => 'ecommerce_wishlist',
'group_label' => __('Wishlist', 'ecommerce'),
'item_id' => 'wishlist-' . $product_id,
'data' => array(
array(
'name' => __('Product Name', 'ecommerce'),
'value' => $product->post_title,
),
array(
'name' => __('Product URL', 'ecommerce'),
'value' => get_permalink($product_id),
),
array(
'name' => __('Price', 'ecommerce'),
'value' => '$' . get_post_meta($product_id, '_price', true),
),
array(
'name' => __('Added to Wishlist', 'ecommerce'),
'value' => $product->post_date,
),
),
);
$data[] = $item_data;
}
return array('data' => $data, 'done' => true);
}
}
// Initialize on plugin load
new Ecommerce_GDPR_Export();
?>
Handling Complex and Sensitive Data
Not all personal data fits neatly into name/value pairs. Here's how to handle complex structures:
Nested/Grouped Data
For related information, use semicolons or custom formatting:
<?php
// Option 1: Semicolon-separated values
array(
'name' => 'Order Items',
'value' => 'Product A (Qty: 2, $30); Product B (Qty: 1, $50)',
)
// Option 2: Structured text with line breaks
array(
'name' => 'Shipping Address',
'value' => "123 Main St\nNew York, NY 10001\nUSA",
)
?>
Sensitive Data
Some data is so sensitive it shouldn't be in exports by default:
- Credit card numbers
- Passwords (never)
- Authentication tokens
- API keys
- Social security numbers
Either exclude them or show redacted versions:
<?php
// Exclude credit card numbers entirely
// Don't include in export
// Or redact the number
array(
'name' => 'Card Number',
'value' => '**** **** **** 4242', // Last 4 digits only
),
// Include payment method but not details
array(
'name' => 'Payment Method',
'value' => 'Visa (stored securely)',
),
?>
Data from External Services
If your plugin stores data fetched from external APIs, you still need to export it:
<?php
// Example: Export data fetched from an email service provider
public function export_email_history($email, $page = 1) {
$user = get_user_by('email', $email);
if (!$user) {
return array('data' => array(), 'done' => true);
}
// Get email history from our local cache/database
global $wpdb;
$emails = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}email_history
WHERE user_email = %s
ORDER BY sent_date DESC
LIMIT 10 OFFSET %d",
$email,
($page - 1) * 10
)
);
$data = array();
foreach ($emails as $email_record) {
$email_data = array(
'group_id' => 'external_emails',
'group_label' => __('Email History', 'my-plugin'),
'item_id' => 'email-' . $email_record->id,
'data' => array(
array(
'name' => __('Email Type', 'my-plugin'),
'value' => $email_record->type,
),
array(
'name' => __('Sent Date', 'my-plugin'),
'value' => $email_record->sent_date,
),
array(
'name' => __('Subject', 'my-plugin'),
'value' => $email_record->subject,
),
array(
'name' => __('Opens', 'my-plugin'),
'value' => $email_record->opens,
),
array(
'name' => __('Clicks', 'my-plugin'),
'value' => $email_record->clicks,
),
),
);
$data[] = $email_data;
}
// Check for more pages
$total = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}email_history
WHERE user_email = %s",
$email
)
);
return array(
'data' => $data,
'done' => (($page * 10) >= $total),
);
}
?>
Testing Your WordPress GDPR Data Export Implementation
Thorough testing ensures your export is complete and accurate:
Unit Tests for Exporter Functions
<?php
/**
* Tests for data export functionality
*/
class Test_GDPR_Export extends WP_UnitTestCase {
public function test_export_returns_array_structure() {
$exporter = new Ecommerce_GDPR_Export();
$result = $exporter->export_customer_data('[email protected]', 1);
$this->assertIsArray($result);
$this->assertArrayHasKey('data', $result);
$this->assertArrayHasKey('done', $result);
}
public function test_export_with_no_user() {
$exporter = new Ecommerce_GDPR_Export();
$result = $exporter->export_customer_data('[email protected]', 1);
$this->assertEmpty($result['data']);
$this->assertTrue($result['done']);
}
public function test_export_with_valid_user() {
$user_id = $this->factory->user->create(array(
'user_email' => '[email protected]',
'first_name' => 'Test',
'last_name' => 'User',
));
$user = get_user_by('ID', $user_id);
$exporter = new Ecommerce_GDPR_Export();
$result = $exporter->export_customer_data($user->user_email, 1);
$this->assertNotEmpty($result['data']);
$this->assertTrue($result['done']);
// Verify data structure
$data = reset($result['data']);
$this->assertArrayHasKey('group_id', $data);
$this->assertArrayHasKey('group_label', $data);
$this->assertArrayHasKey('item_id', $data);
$this->assertArrayHasKey('data', $data);
}
public function test_pagination_works() {
$user_id = $this->factory->user->create();
// Create 15 orders
global $wpdb;
for ($i = 0; $i < 15; $i++) {
$wpdb->insert(
$wpdb->prefix . 'ecommerce_orders',
array(
'user_id' => $user_id,
'order_number' => 'ORD-' . $i,
'created_at' => current_time('mysql'),
'status' => 'completed',
'total' => 99.99,
),
array('%d', '%s', '%s', '%s', '%f')
);
}
$user = get_user_by('ID', $user_id);
$exporter = new Ecommerce_GDPR_Export();
// Page 1: 10 items
$result = $exporter->export_order_data($user->user_email, 1);
$this->assertCount(10, $result['data']);
$this->assertFalse($result['done']);
// Page 2: 5 items
$result = $exporter->export_order_data($user->user_email, 2);
$this->assertCount(5, $result['data']);
$this->assertTrue($result['done']);
}
}
?>
Manual Testing Checklist
- Create test user with sample data in each data exporter category
- In WordPress admin, go to Tools → Export Personal Data
- Enter the test user's email
- Click "Send Confirmation Email"
- Follow link from email
- Click "Download Personal Data"
- Extract the ZIP file
- Verify all data categories are present
- Verify data accuracy against database
- Test with edge cases:
- User with no data in certain categories
- User with large amounts of data (tests pagination)
- User with special characters in data
- User with multimedia data
Verify with WP HealthKit
Upload your plugin to WP HealthKit for automated audit. We check for:
- Missing exporter registrations
- Incomplete data structures
- Pagination implementation
- Sensitive data exposure
- /upload for instant analysis.
Creating a GDPR Data Export Checklist
For compliance documentation, create a checklist of all personal data your site collects:
GDPR Article 20 Compliance Checklist
Plugin: My E-commerce Plugin
Last Reviewed: 2026-03-18
DATA COLLECTED:
- User account information (name, email, registration date)
- Billing address (street, city, state, zip, country, phone)
- Shipping addresses (multiple)
- Order history (order number, date, items, total, status)
- Payment information (method only, not card details)
- Product reviews (text, rating, date)
- Wishlist (product IDs, added date)
- Email engagement (opens, clicks, dates)
EXPORTERS IMPLEMENTED:
✓ Customer account data exporter
✓ Order data exporter (with pagination)
✓ Review data exporter
✓ Wishlist exporter
✓ Email history exporter
EXCLUDED DATA:
- Payment card numbers (stored only in PCI-compliant vault)
- Authentication tokens
- API keys
- Password hashes
SENSITIVE DATA HANDLING:
- Card numbers redacted (last 4 digits only)
- No plain-text passwords included
- Billing address combined into single field
TESTING:
✓ Unit tests for all exporters
✓ Pagination testing
✓ Manual export verification
✓ Edge case testing
COMPLIANCE:
✓ Documented all exporters
✓ Tested with WP HealthKit
✓ Verified data accuracy
✓ Ready for GDPR audits
Additional Resources
GDPR compliance isn't optional for plugins that handle user data. The General Data Protection Regulation applies to any website processing data of European Union residents, regardless of where the website is hosted or the company's location. Violations can result in fines up to 20 million euros or 4% of global revenue—whichever is higher. For a SaaS company or agency managing client sites, a single GDPR violation could be catastrophic.
WordPress provides built-in tools for data export and deletion (the Personal Data tools), but many plugins handle additional data that these tools don't cover. If your plugin stores custom user data—preferences, activity logs, relationship data, file metadata—you must provide data export and deletion functionality specifically for that data. This isn't a feature request, it's a legal requirement.
The challenge is implementing export and deletion properly. You must find all data associated with a user, export it in a readable format, and delete it completely. This gets complicated with multiple database tables, custom post types, post metadata, options, and third-party integrations. A partial implementation that misses data categories violates GDPR. By implementing comprehensive export and deletion, you ensure legal compliance and build user trust.
Frequently Asked Questions
What's the difference between personal data export and account deletion?
Export gives users a copy of their data. Deletion removes it. GDPR Article 20 requires export; Article 17 requires erasure. Both are separate operations that must be implemented independently.
Should I export data from third-party services?
If you store it locally, yes. If you only have a reference to external data (like a customer ID), you can either include the reference or note that data is stored externally. The key is transparency.
Can I charge for personal data exports?
GDPR allows charging only if requests are repetitive or manifestly unfounded. One export per year is typically free. Your process should be free by default.
What file format should the export be in?
WordPress creates a ZIP containing an HTML file. The HTML is readable and printable. You could also generate CSV or JSON formats for tech-savvy users.
How long should I keep export logs?
There's no specific GDPR requirement, but keeping logs of export requests for 30 days helps demonstrate compliance.
Should I export deleted/anonymized data?
GDPR doesn't require exporting data that's already been deleted or anonymized. Include only currently stored personal data.
Best Practices for WordPress GDPR Data Export
Document Everything
Your exporters should be self-documenting:
<?php
// Clear, descriptive function names
function export_customer_contact_information($email, $page = 1) { }
// Informative group labels
'group_label' => __('Customer Contact Information', 'my-plugin'),
// Descriptive field names
'name' => __('Telephone Number (Billing Address)', 'my-plugin'),
?>
Handle Edge Cases
Users might:
- Have no data
- Have data spanning multiple pages
- Have mixed case email addresses
- Have deleted accounts (but export still requested)
Handle each gracefully:
<?php
public function export_data($email, $page = 1) {
// Normalize email
$email = sanitize_email(strtolower($email));
$user = get_user_by('email', $email);
// User not found - return empty but valid response
if (!$user) {
return array('data' => array(), 'done' => true);
}
// User found but no data - return empty but valid response
$user_data = get_user_meta($user->ID, 'my_plugin_data', true);
if (empty($user_data)) {
return array('data' => array(), 'done' => true);
}
// ... export logic
}
?>
Exclude Unnecessary Data
Not everything is personal data. Don't export:
- Plugin settings
- Site configuration
- System information
- Logs (unless user-specific)
Export only data that identifies or relates to the individual.
Use Consistent Formatting
Format dates, numbers, and currencies consistently:
<?php
// Date format - use ISO 8601
'value' => date('Y-m-d H:i:s', strtotime($timestamp)),
// Currency - use ISO 4217 with consistent formatting
'value' => 'USD ' . number_format($amount, 2),
// URLs - use full absolute URLs
'value' => add_query_arg('product', $id, home_url('products')),
?>
Compliance Timeline and Process
GDPR requires responding to personal data export requests within 30 days. Here's a recommended workflow:
Day 1: User submits export request (via admin form or API) Day 2: Admin confirms request via email link (verification step) Day 3-28: System generates export, sends to user Day 29: Confirm user received file Day 30: Archive confirmation for compliance records
WordPress's Export Personal Data feature automates much of this, but you should still:
- Log all export requests
- Verify user identity
- Provide clear instructions
- Follow up if user doesn't confirm
Data Export Formats
Export data must be in a format users can actually use. The WordPress standard is WXR (WordPress eXtended RSS) XML format for post data, but user data might be better in CSV or JSON formats that users can import into other systems. Different data types have different appropriate export formats.
The critical requirement is that exported data must be understandable to the user. Exporting database dumps or serialized PHP objects isn't acceptable—users can't read those formats. Export raw, understandable text or structured formats like JSON or CSV. Include metadata explaining what each field means.
Additionally, consider whether your export includes all related data. If your plugin associates files with user accounts, the export should include file references. If it tracks relationships, the export should show those relationships. A complete export should allow someone to understand and potentially recreate everything your plugin knows about them.
Privacy Impact Assessments
Before implementing data export and deletion, conduct a privacy impact assessment. Document what data your plugin collects, why, how long you retain it, and who can access it. This assessment helps ensure GDPR compliance and builds user trust.
Share privacy information transparently. Your plugin's settings should explain what data is collected. Help users understand their privacy rights. By being transparent about data practices, you demonstrate respect for user privacy.
Finally, consider data minimization. Collect only data you actually need. Retain only for as long as necessary. Delete proactively when data is no longer needed. By minimizing data collection and retention, you reduce privacy risks.
Right to Data Portability and Format Standards
GDPR's right to data portability requires that users can obtain their data in a structured, commonly used, machine-readable format suitable for transmission to another service. The WordPress Core Data Exporter was built specifically to address this requirement. It generates ZIP files containing JSON and CSV exports of user data. The law implicitly accepts CSV and JSON as suitable formats, but the implementation must ensure that exported data is complete and accurate. Many plugins export only basic profile information while omitting related data from custom post types or database tables. This creates GDPR compliance gaps when exported data is incomplete.
Exporting Related Data and Relationships
Real compliance requires exporting not just the user record itself but all related data they've generated. For an e-commerce site, this means order history, payment information (though PCI-DSS restrictions apply), shipping addresses, and product reviews. For a community platform, it means comments, forum posts, connections, and activity logs. The WordPress Personal Data Exporter framework provides hooks that third-party plugins can use to register their exportable data, but many plugins forget to implement these hooks. Users requesting their data then receive incomplete exports that don't represent their actual activity on the site.
Anonymization Versus Deletion Trade-offs
Some regulations distinguish between deletion (completely removing identifiable data) and anonymization (removing the connection between data and identity while retaining the data itself). A user's published comment might need to be anonymized so it remains visible as community content but no longer connects to their profile. WordPress's Personal Data Eraser can be configured to delete or anonymize, but the choice depends on legitimate business interests and legal obligations. GDPR allows retention of anonymized data if genuine anonymization has been applied, whereas deletion must be complete and verifiable.
Third-Party Service Data and Consent Records
Many WordPress sites integrate with external services like email marketing platforms, analytics providers, CRM systems, and payment processors. When users request data export, you must include information about what data has been shared with these third parties. GDPR requires disclosing which external organizations have received personal data. This often means integrating with those external services to fetch exported data about your users. A user's complete data export should theoretically include their email marketing history, analytics profile, and customer records from integrated services. Realistically, this requires API integrations with each third party.
Exporting Logs and Activity History
Users increasingly expect access to their complete activity history when exporting data. This includes login times, password reset requests, content modifications, and access logs. WordPress doesn't natively track this data, but security plugins often do. The Personal Data Exporter must integrate with all such logging mechanisms to export complete activity records. Omitting activity logs from exports creates incomplete data exports that violate the spirit of data portability requirements.
Conclusion
WordPress GDPR data export is a legal requirement that also serves users. By implementing comprehensive exporters for all personal data your plugins handle, you demonstrate respect for user privacy and prepare for compliance audits.
The key points:
- Register exporters for all data your plugin collects
- Follow the data format standard precisely
- Test with real user data
- Handle pagination for large datasets
- Document your exporters clearly
Getting this right builds trust with users. When people see that they can easily retrieve their data, it proves you take privacy seriously.
Make sure your implementation is complete and auditable. Upload your plugin to WP HealthKit for an instant GDPR compliance check. We verify that all personal data is being exported correctly, sensitive data is handled properly, and your implementation follows Article 20 requirements.
Ensure your plugin meets GDPR Article 20 requirements. Upload to WP HealthKit for an automated GDPR data export compliance audit.
Respecting User Rights
GDPR and similar regulations exist because users deserve control over their data. By implementing proper data export and deletion, you respect user rights. You acknowledge that data belongs to users, not to you. This respect builds long-term trust. Users are more willing to use plugins they trust to handle their data responsibly. By implementing GDPR compliance properly, you build a reputation for privacy and responsibility that helps your plugin grow. WP HealthKit checks whether your plugin implements GDPR-compliant data export and deletion functionality. Our analysis verifies that your plugin respects user rights and provides proper data portability. GDPR compliance isn't optional for plugins handling user data.
Users increasingly expect their data to be portable and deletable. By implementing these features properly, you respect user autonomy and build trust. GDPR compliance is becoming standard practice and helps establish your plugin as responsible and trustworthy.
Upload your plugin to WP HealthKit to verify GDPR compliance and data handling practices. Data handling practices build user trust. GDPR compliance demonstrates that you respect user rights and take privacy seriously. Users increasingly expect to control their data. By providing export and deletion functionality, you show that you acknowledge this right. This builds goodwill with users and protects you legally. Data responsibility is becoming standard practice for trustworthy companies. By implementing GDPR compliance, you position your plugin as responsible and trustworthy. Document your data handling practices clearly. Tell users what data you collect, why, how long you keep it, and who can access it. Transparency builds trust and helps users understand their rights. GDPR compliance demonstrates responsibility. Users trust plugins respecting their privacy.