Skip to content
EDD plugins

How to Customize EDD Email Templates (Complete Developer Guide)

· · 13 min read
Developer customizing EDD email templates with PHP code on dark monitors

Easy Digital Downloads ships with functional, workable email notifications out of the box. But “workable” and “on-brand” are two very different things. If you’ve ever wanted to swap EDD’s default purchase receipt for something that matches your store’s identity: custom colors, your logo, a personalized message, conditional content per product. This guide covers exactly how to do that at the PHP level.

We’ll walk through the templating system EDD uses, the core hooks and filters available, and how to build a fully custom email template without touching the plugin files directly. Everything here uses EDD’s extension API, which means your changes survive updates.


How EDD’s Email System Works

Before writing a line of code, it’s worth understanding EDD’s email architecture. This saves you from overriding the wrong hooks and helps you target exactly the output you want to change.

EDD handles transactional emails through the EDD_Emails class, located in includes/emails/class-edd-emails.php. This class manages the email header, body, and footer as separate template parts. The class wraps content in an HTML envelope and sends it via wp_mail().

The Three Template Parts

  • Header: The opening HTML, doctype, <head> block, and the email header image area.
  • Body: The dynamic content specific to each email type (purchase receipt, sale notification, etc.).
  • Footer: The closing HTML and footer text configured in EDD settings.

Each of these parts is filterable. EDD also supports full template file overrides, following the same pattern as WooCommerce: you copy template files into your theme or a custom plugin, and EDD loads yours instead of its own.


Method 1: Template File Overrides

The cleanest way to customize EDD email templates is to override the template files. EDD checks your active theme and parent theme for template overrides before loading its own files.

Where EDD Looks for Templates

EDD loads email templates from two locations, in order:

  1. yourtheme/edd_templates/emails/
  2. yourtheme/edd/emails/
  3. edd/includes/emails/email-template.php (plugin default)

This means you can create a folder called edd_templates/emails/ in your theme directory, drop in a customized copy of email-template.php, and EDD will automatically pick it up.

# Copy the default template into your theme
cp wp-content/plugins/easy-digital-downloads/includes/emails/email-template.php \
   wp-content/themes/your-theme/edd_templates/emails/email-template.php

Open your copy and you’ll see it’s a standard PHP/HTML file. You can change the table structure, add inline styles, add a logo, or restructure the layout entirely.

Adding a Custom Logo

Inside the copied template file, locate the header section. The default output is driven by the edd_email_header action. You can replace it with your own output:

<?php
// In your theme's edd_templates/emails/email-header.php
$logo_url = get_stylesheet_directory_uri() . '/images/email-logo.png';
?>
<table width="600" cellpadding="0" cellspacing="0" border="0" align="center">
  <tr>
    <td align="center" style="padding: 20px 0;">
      <img src="<?php echo esc_url( $logo_url ); ?>"
           alt="<?php echo esc_attr( get_bloginfo('name') ); ?>"
           width="200" style="display:block;" />
    </td>
  </tr>
</table>

Always use absolute URLs for images in emails. Relative paths will break in email clients. Use get_stylesheet_directory_uri() for theme images, or store the logo URL in a theme option and pull it in.


Method 2: Filters and Actions for Fine-Grained Control

If you only need to tweak specific parts of the email output rather than replace the entire template, EDD provides a comprehensive set of hooks. This approach is better for small-to-medium customizations and keeps your code lean.

The Most Useful Email Filters

A quick correction worth flagging up front: several EDD email tutorials online, including earlier versions of this one, list edd_email_header_output and edd_email_footer_output as filters. Those hook names do not exist in the EDD codebase. The header and footer are actually fired as actions, not filters, which changes how you hook into them. The table below reflects the hooks actually present in includes/emails/class-edd-emails.php and related files as of this writing.

HookTypeWhat It Controls
edd_email_headerActionFires where the header HTML should be echoed
edd_email_footerActionFires where the footer HTML should be echoed
edd_email_messageFilterThe main body content string; return the modified string
edd_purchase_subjectFilterSubject line for the purchase receipt email
edd_email_headingFilterThe heading text shown at the top of the email body
edd_email_from_nameFilterSender display name
edd_email_from_addressFilterSender email address

Customizing the Email Body

The edd_email_message filter runs on the body content before it’s wrapped in the email shell. Use it to append or prepend content:

add_filter( 'edd_email_message', 'my_custom_edd_email_message', 10, 2 );

function my_custom_edd_email_message( $message, $payment_data ) {
    // Append a custom support note at the end of the receipt
    $message .= '<p style="color:#555;font-size:14px;">';
    $message .= 'Questions? Reply to this email or visit our <a href="' . esc_url( home_url('/support/') ) . '">Support Center</a>.';
    $message .= '</p>';

    return $message;
}

The second parameter $payment_data gives you access to the full payment object, so you can make the message conditional based on what was purchased, the total amount, customer information, or anything else stored with the payment.

Replacing the Header Entirely

edd_email_header fires as an action at the point in the template where the header should print, not a filter you return a string from. That means your callback should echo output directly rather than returning it. If you want a full replacement rather than an addition, remove any existing callbacks on the hook first so you don’t end up with two headers stacked on top of each other:

add_action( 'edd_email_header', 'my_custom_edd_header', 5 );

function my_custom_edd_header( $email ) {
    // Remove any other callbacks still hooked at this point, so only ours prints.
    remove_all_actions( 'edd_email_header' );
    add_action( 'edd_email_header', 'my_custom_edd_header_output' );
}

function my_custom_edd_header_output() {
    ?>
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
        <title><?php echo esc_html( get_bloginfo('name') ); ?></title>
        <style type="text/css">
            body { margin: 0; padding: 0; background-color: #f7f7f7; }
            table { border-collapse: collapse; }
            .header { background-color: #1a1a2e; padding: 24px 40px; }
        </style>
    </head>
    <body>
    <table width="100%" cellpadding="0" cellspacing="0">
        <tr><td>
            <table class="header" align="center" width="600" cellpadding="0" cellspacing="0">
                <tr>
                    <td align="center">
                        <img src="<?php echo esc_url( get_stylesheet_directory_uri() . '/images/logo.png' ); ?>"
                             alt="<?php echo esc_attr( get_bloginfo('name') ); ?>" />
                    </td>
                </tr>
            </table>
    <?php
}

In practice, most stores get cleaner, safer results from the template file override in Method 1 whenever the change is this structural. Reach for the action hook above for smaller header tweaks (swapping a background color or logo), and reach for a full template override when you’re rebuilding the header’s HTML structure from scratch, since a template override can’t collide with another plugin’s callback on the same action.


Method 3: Adding Custom Template Tags

EDD’s email system supports template tags: placeholders like {name}, {download_list}, and {sitename} that are replaced with dynamic values when the email sends. You can register your own tags by hooking into the edd_add_email_tags action, which is where EDD registers its own built-in tags before the email content is generated.

This is particularly useful for EDD stores that sell software licenses, subscription products, or anything with custom metadata. If you are building a licensing system, see our guide on setting up EDD software licensing. The license key tag approach below pairs well with that setup. If you store extra information with each payment, you can surface it in the email via a custom tag.

Registering a Custom Tag

add_action( 'edd_add_email_tags', 'register_my_custom_email_tags' );

function register_my_custom_email_tags() {
    edd_add_email_tag( 'license_key', 'The customer license key', 'my_get_license_key_tag' );
    edd_add_email_tag( 'access_expires', 'When download access expires', 'my_get_access_expiry_tag' );
}

function my_get_license_key_tag( $payment_id ) {
    $license_key = get_post_meta( $payment_id, '_edd_sl_key', true );
    return $license_key ? esc_html( $license_key ) : 'N/A';
}

function my_get_access_expiry_tag( $payment_id ) {
    $expiry = get_post_meta( $payment_id, '_edd_download_expiry', true );
    if ( ! $expiry ) {
        return 'Lifetime access';
    }
    return date_i18n( get_option('date_format'), strtotime( $expiry ) );
}

Once registered, you can use {license_key} and {access_expires} anywhere in the EDD email settings: subject line, header text, body, or footer. This eliminates the need to hard-code dynamic values into your template override.


Method 4: Product-Specific Email Content

One of the most powerful things you can do with EDD’s email system is vary the content based on what was purchased. If you sell both beginner courses and advanced developer tools, the post-purchase messaging for each should look and feel different.

EDD fires edd_email_message with the full $payment_data array. Within that, check $payment_data['downloads'] to see which products are in the order.

add_filter( 'edd_email_message', 'edd_conditional_product_email_content', 10, 2 );

function edd_conditional_product_email_content( $message, $payment_data ) {
    if ( empty( $payment_data['downloads'] ) ) {
        return $message;
    }

    $developer_products = array( 42, 87, 113 );
    $beginner_products  = array( 15, 16, 19 );
    $purchased_ids = wp_list_pluck( $payment_data['downloads'], 'id' );

    $has_developer = ! empty( array_intersect( $purchased_ids, $developer_products ) );
    $has_beginner  = ! empty( array_intersect( $purchased_ids, $beginner_products ) );

    if ( $has_developer ) {
        $message .= '<hr />';
        $message .= '<h3>Getting Started as a Developer</h3>';
        $message .= '<p>Your download includes full source code and access to our developer Slack channel.</p>';
    }

    if ( $has_beginner ) {
        $message .= '<hr />';
        $message .= '<h3>Welcome, Here\'s Where to Start</h3>';
        $message .= '<p>Start with Module 1 of the course. <a href="' . esc_url( home_url('/getting-started/') ) . '">Your learning dashboard is here</a>.</p>';
    }

    return $message;
}

You can also check product categories instead of specific IDs, which is more maintainable as your catalog grows. Use get_the_terms( $download_id, 'download_category' ) inside the loop to inspect product taxonomy.


Method 5: Building a Custom Email Type

Beyond purchase receipts, you may want to send entirely custom transactional emails triggered by your own plugin logic: renewal reminders, license expiration warnings, or download limit notifications. For stores using subscriptions, our EDD recurring payments guide covers the subscription lifecycle where these custom emails are most valuable. EDD provides the EDD_Emails class for this purpose.

Sending Custom Emails via EDD’s Infrastructure

function send_renewal_reminder( $customer_id, $payment_id ) {
    $customer = new EDD_Customer( $customer_id );

    if ( ! $customer || empty( $customer->email ) ) {
        return;
    }

    EDD()->emails->__set( 'from_name',    get_bloginfo('name') );
    EDD()->emails->__set( 'from_address', get_option('admin_email') );
    EDD()->emails->__set( 'heading',      'Your License Is Expiring Soon' );

    $subject = sprintf(
        'Action needed: Your %s license expires in 7 days',
        get_bloginfo('name')
    );

    $message  = '<p>Hello ' . esc_html( $customer->name ) . ',</p>';
    $message .= '<p>Your software license expires in 7 days. Renew now to keep updates and support.</p>';
    $message .= '<p><a href="' . esc_url( home_url('/renew/?payment=' . $payment_id) ) . '" ';
    $message .= 'style="background:#0073aa;color:#fff;padding:12px 24px;text-decoration:none;border-radius:4px;">';
    $message .= 'Renew My License</a></p>';

    EDD()->emails->send( $customer->email, $subject, $message );
}

This uses EDD’s email infrastructure, so the message gets wrapped in your customized template automatically. Your logo, color scheme, and footer text all appear as configured, without duplicating any templating code.


Inline CSS and Email Client Compatibility

One pain point that catches many developers: email clients strip <style> blocks. Gmail ignores anything inside a <style> tag. All styling must be written inline on each element.

Best Practices for EDD Email CSS

  • Write all CSS inline: style="color: #333; font-size: 16px;"
  • Use table-based layouts for consistent rendering across Outlook and Gmail
  • Set explicit widths on all table cells and images
  • Avoid CSS shorthand: use font-size, font-family, font-weight as separate properties
  • Test in Litmus or Email on Acid before deploying
  • Use system fonts (Arial, sans-serif) to avoid web font loading issues

If you want to write normal CSS and auto-inline it, consider using a PHP library like tijsverkoyen/CssToInlineStyles. Hook it into edd_email_message and pass the body through the inliner before the email sends.


Testing Your Email Customizations

EDD includes a built-in email preview tool. Navigate to Downloads > Settings > Emails and click the “Preview” button on the purchase receipt. This renders the full email in the browser using a sample payment, which is the fastest way to iterate on template changes without sending real emails.

Triggering Test Emails via WP-CLI

# Send a test purchase receipt for payment ID 1234
wp eval "edd_trigger_purchase_receipt(1234);"

# Send to a specific address to verify delivery
wp eval "
  \$payment = new EDD_Payment(1234);
  EDD()->emails->send(
    '[email protected]',
    'Test Receipt',
    edd_get_purchase_receipt_email_content(\$payment->ID, array())
  );
"

Use Mailtrap in your development environment to catch all outgoing emails without sending them to real inboxes. Configure it via the WP Mail SMTP plugin.


Organizing Customizations as a Must-Use Plugin

If you are managing multiple EDD email customizations, package them in a must-use plugin rather than scattering code across your theme’s functions.php. This keeps your logic organized, theme-independent, and easier to maintain or hand off to another developer.

<?php
/**
 * Plugin Name: My Store Email Customizations
 * Description: Custom EDD email templates, hooks, and tags.
 * Version: 1.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

if ( ! class_exists( 'Easy_Digital_Downloads' ) ) {
    return;
}

require_once plugin_dir_path( __FILE__ ) . 'includes/email-tags.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/email-filters.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/email-conditional.php';

Place this plugin file in wp-content/mu-plugins/my-store-emails.php. WordPress loads mu-plugins on every request automatically, and they cannot be accidentally deactivated from the plugins screen. Each category of customization lives in its own include, easy to locate, debug, and extend independently.


Which Method Should You Use?

Customization GoalRecommended Approach
Change overall layout and brandingTemplate file override in theme
Add or modify specific content blocksedd_email_message filter
Change header or footer HTMLedd_email_header / edd_email_footer actions
Add dynamic placeholdersCustom tags via the edd_add_email_tags action and edd_add_email_tag()
Vary content per product purchasededd_email_message + $payment_data check
Send a custom transactional email typeEDD()->emails->send() directly

EDD’s email system is more extensible than most developers realize. The hooks and template overrides built into the free plugin cover the vast majority of real-world use cases. You don’t need a premium extension for basic branding, conditional content, or custom email types. The five methods above handle all of it.


Common Mistakes When Customizing EDD Emails

The most frequent mistake is editing the plugin’s own template files directly inside wp-content/plugins/easy-digital-downloads/ instead of copying them into a theme or must-use plugin first. Any edit made straight to the plugin folder disappears the next time EDD updates, silently reverting your branding without any warning. Always work from a copy in your theme’s edd_templates/emails/ directory or from hooks in a must-use plugin, never from the plugin’s own files.

A second common mistake is confusing filters and actions, which is exactly the error this guide corrected above with the header and footer hooks. If your custom function isn’t changing anything, check whether the hook you’re using is a filter (your callback must return a value) or an action (your callback should echo output directly). Hooking a filter callback to an action, or vice versa, fails silently in most cases rather than throwing a visible error, which makes this class of bug slow to track down without knowing to check it first.

A third mistake is testing email changes only through the built-in preview tool and never through an actual send. The preview renders the template with sample data, but it doesn’t catch every real-world issue, particularly around how different email clients strip or mangle CSS. Always send at least one real test email, ideally through a tool like Mailtrap so it doesn’t land in a live customer’s inbox, before considering a template change finished.

FAQ

Will customizing EDD emails with hooks survive a plugin update?

Yes, as long as your customizations live in your theme or a separate must-use plugin rather than inside the Easy Digital Downloads plugin folder itself. Hooks and filters attach from outside the plugin, so an EDD update replaces the plugin’s own files without touching anything you’ve added through add_filter(), add_action(), or a template file override in your theme.

Do I need a premium EDD extension to customize emails?

No. Everything covered in this guide, template overrides, filters, custom template tags, conditional per-product content, and fully custom transactional email types, works with the free version of Easy Digital Downloads. Premium extensions add convenience UIs for some of this, but the underlying hooks are available without one.

Why isn’t my custom filter or action doing anything?

The most common cause is exactly the mistake this guide corrected for the header and footer: treating an action hook like a filter, or the reverse. Double-check the hook type before assuming your code is broken. It’s also worth confirming your must-use plugin or theme’s functions.php is actually loading, since a PHP fatal error elsewhere in the file can silently prevent your hook registration from ever running.

Can I use Markdown or a page builder instead of raw HTML for email content?

EDD’s email body ultimately expects HTML, since that’s what gets wrapped in the email’s HTML envelope and sent via wp_mail(). You can generate that HTML however you like, including from a Markdown parser or a page builder’s rendered output, as long as your edd_email_message filter callback returns a valid HTML string with styles written inline rather than in a <style> block, since most email clients strip those.

Take Your EDD Store Further

Customizing transactional emails is one piece of a well-optimized EDD store. If you are looking to extend EDD with custom checkout flows, licensing logic, subscription management, or third-party service integrations, our team specializes in EDD-specific development. Get in touch to discuss your project requirements.

Leave a Reply

Your email address will not be published. Required fields are marked *