Custom download delivery methods in Easy Digital Downloads - server rack representing CDN and file delivery infrastructure

How to Create Custom Download Delivery Methods in EDD for Unique Product Types

When you sell digital products with Easy Digital Downloads, the default file delivery works well for simple use cases: a customer pays, EDD generates a download link, and the file transfers directly. But what happens when your products don’t fit that mold? Software licenses that need API keys instead of files. Video courses that should stream without exposing the raw file URL. Large assets that need CDN delivery for speed and reliability. Time-gated downloads that expire after a set window to reduce piracy.

EDD’s hook system makes all of this possible. The plugin exposes a set of filters and actions that let you intercept, modify, or completely replace the default delivery process – without patching core files or maintaining a fork. This article walks through six custom delivery patterns, with working code for each, that cover the most common scenarios you’ll encounter building specialized EDD stores.

This is article 3 in the EDD Custom Development series. If you missed the earlier articles on setting up recurring payments in EDD and building EDD webhook automation, those are worth reviewing first. The patterns here build on the same hook-first philosophy: extend EDD through its API rather than modifying its source.


Understanding the EDD Download Delivery Flow

Before customizing delivery, it helps to understand what EDD does by default. When a customer clicks a download link in their receipt or on the purchase confirmation page, EDD validates the request through several checks before serving the file. It verifies the payment key, checks that the email matches, confirms the download hasn’t expired, and validates the file key. Only after all checks pass does EDD redirect the browser to the actual file URL or serve it directly.

The core action that powers this is edd_process_verified_download. This fires after all the validation has passed, with the download ID, customer email, payment ID, and any extra arguments available. By hooking into this action with a priority lower than 10 (EDD’s default), you can intercept the process and redirect to your own delivery logic before EDD’s default runs.

The key filters you’ll use throughout this article are:

  • edd_process_verified_download – action that fires after verification, before actual delivery
  • edd_download_file_url – filter to modify the download URL EDD generates
  • edd_download_file_url_args – filter to modify the arguments used in URL generation
  • edd_receipt_show_download_files – filter to control whether file links appear in receipts
  • edd_download_files – filter to modify the files array for a given download
  • edd_template_paths – filter to add custom template directories

A custom meta field on each download product – _edd_delivery_method – will be the routing key throughout this article. When the value is streaming, custom_cdn, api_credentials, or token_limited, your custom delivery logic kicks in. When the field is empty, EDD handles everything normally.


The Delivery Hook Architecture

The foundation of any custom EDD delivery system is registering hooks that intercept the right part of the delivery process. The code below shows the base structure: a filter that modifies URL arguments based on the delivery method, and an action that intercepts the verified download event and handles CDN-based delivery.

The pattern here is intentional. Rather than hooking at priority 10 (which would conflict with EDD’s own handler), you hook at priority 5. When your function detects the custom delivery method in the post meta, it handles the request and exits with exit. When the delivery method is absent or doesn’t match, your function returns early and EDD’s default handler runs at priority 10 as normal.

This approach has one important practical benefit: your custom delivery code runs on the same verified, validated request that EDD already processed. You’re not bypassing security – you’re extending the pipeline that runs after security checks pass.

Always call edd_record_download_in_log() before serving your custom delivery. If EDD’s delivery log doesn’t have a record, the download limit system won’t work correctly – customers could bypass per-purchase download limits.


Streaming File Delivery

The default EDD delivery redirects browsers to the actual file URL – whether that’s a local path formatted as a URL, a protected uploads directory URL, or a remote URL on S3 or another host. This means the file location is visible in the browser’s address bar and download manager, making it easier for customers to share direct links or bypass your download limit controls.

Streaming delivery solves this by having PHP read the file and pipe it directly to the browser. The customer’s download manager shows your site’s URL, not the storage location. The real file path never leaves your server.

When to use streaming delivery

  • Products stored in a non-public directory on the server (outside of wp-content/uploads)
  • Products where you want to prevent direct URL sharing
  • Files served from private S3 buckets where you control access via PHP
  • Situations where you need to log or rate-limit downloads at the application layer

What streaming delivery doesn’t solve

Streaming keeps the URL private, but once the file is on the customer’s machine, it’s a normal file. If piracy prevention is your main concern, consider pairing streaming delivery with time-limited tokens (covered later in this article) so download links expire quickly after the first use.

Streaming also puts the file transfer load on your PHP process. For large files (over 50MB) or high-traffic stores, CDN delivery is a better fit. Use streaming for smaller files or low-to-medium traffic scenarios.

A few implementation details worth noting. The set_time_limit(0) call is important for large files – PHP’s default execution limit will kill the connection mid-transfer otherwise. The chunk-based read loop with flush() keeps memory usage flat regardless of file size, since you’re never loading the entire file into memory. The ob_end_clean() loop at the start clears any output buffering that WordPress or plugins may have started, which would otherwise cause the file to buffer completely before sending.

The MIME type detection with mime_content_type() is a nice touch for mixed-product stores. Rather than hardcoding application/octet-stream (which causes some browsers to open a save dialog unnecessarily), you get the correct type for each file. PDFs open in the browser PDF viewer, images preview inline, and zip files trigger a download prompt – all appropriate behavior.


External Service Delivery – API Keys and Credentials

Not every digital product is a file. Software plugins and SaaS products often deliver access credentials – an API key, a license key, or a username/password combination – rather than a downloadable asset. EDD supports file-based downloads natively, but with a bit of code, you can use it as the payment and customer management layer while delivering credentials instead of files.

The pattern has two parts. First, generate the credentials on purchase completion and store them against the payment. Second, replace the file download links in the purchase receipt with a credentials display.

Connecting credentials to your external service

The code above generates and stores an API key, but for a real integration you’d also need to provision access in your external service. The edd_complete_purchase hook is the right place to do this – it fires once after payment confirmation, so you won’t provision duplicate credentials on page refreshes.

From within eddsell_deliver_api_credentials(), you can make an HTTP request to your service’s provisioning API using wp_remote_post(). Store the response (the API key your service generates, not one you generate yourself) in payment meta for display in the receipt. This approach works with any external service that has a REST API for user provisioning. If your product is a WordPress plugin that customers install on their own sites, the EDD Software Licensing addon handles the full license key delivery and plugin update delivery pipeline without custom code.

Scenario Credential Type Storage Location Display In
SaaS subscription Username + password Payment meta + user meta Receipt email + account page
WordPress plugin license License key Payment meta Receipt + EDD licenses page
REST API access Bearer token Payment meta (encrypted) Custom account dashboard
Course platform Enrollment token User meta Auto-redirect to course platform

Security considerations for credential delivery

API keys and credentials in purchase receipts present a different security profile than file downloads. A stolen receipt email gives an attacker immediate access to whatever service the key unlocks. A few mitigations worth building in from the start:

  • Store sensitive keys encrypted in the database using openssl_encrypt() with a server-side key, not plain text in payment meta
  • Show the key in the receipt once, then show a “View in account” link for subsequent access that requires the customer to be logged in
  • Add a key rotation endpoint in the customer’s account area – if a key is compromised, they can invalidate and reissue without a refund workflow
  • Log credential views with IP and timestamp so you have an audit trail if a key is misused

Time-Limited Access Tokens for Downloads

EDD’s standard download links include a payment key and email in the query string and expire based on a configurable download expiry date (usually 72 hours by default). For products where you need tighter control – a 15-minute download window after the customer clicks, for instance – you need a custom token system.

Time-limited tokens use HMAC signing to create self-contained, verifiable URLs. The URL carries a base64-encoded payload (download ID, payment ID, file key, expiry timestamp) plus an HMAC signature. The server can verify the signature and check the expiry without a database lookup on every request – the token itself contains all the information needed.

Choosing the right token lifetime

Token lifetime is a balance between security and usability. Very short windows (under 5 minutes) frustrate customers on slow connections. Too long (over 24 hours) defeats the purpose of token-based delivery. In practice, 15 to 30 minutes works well for most digital product stores – enough time for the customer to click through, start the download, and complete it even on a slow connection.

For large files where the download itself might take longer than the token window, there are two approaches. You can check the expiry only at the start of the download (when the request hits your handler), not continuously during the transfer. This means a customer can start a download within the window and complete it even if the token expires during the transfer. Alternatively, for CDN-based delivery where the CDN handles the actual transfer, the CDN URL expiry should be set to start-of-download-plus-transfer-time, not just a fixed window.

Token rotation after single use

The code above only checks token validity (signature and expiry) but doesn’t prevent a token from being used multiple times within its window. For true single-use tokens, store a hash of used tokens in a transient or a custom database table, and reject any token whose hash is already in the store. Set the transient expiry to match the token window so the store doesn’t grow indefinitely.

Implementing this adds a database read on every download request. For most stores, the transient lookup is fast enough that this isn’t a concern. For high-volume stores, consider a Redis-backed transient store or a dedicated token invalidation table with an index on the token hash column.


CDN Integration for File Delivery

For stores selling large files – video courses, high-resolution asset packs, software distributions – serving files from your WordPress server’s storage is a bottleneck. PHP streaming scales poorly under load, and the bandwidth costs from your hosting provider add up fast. A CDN changes the math: files are served from edge nodes close to the customer, transfers are fast even for large files, and your origin server only handles the signed URL generation, not the actual file transfer.

The most common setup pairs S3 for file storage with CloudFront for delivery. Files are uploaded to a private S3 bucket (no public access), and your PHP code generates signed CloudFront URLs that grant temporary access to specific files. The customer’s browser downloads directly from CloudFront. Your WordPress server’s PHP never handles the file bytes.

Setting up the CloudFront key pair

CloudFront signed URLs require a key pair created in your AWS account’s root user settings (not IAM). This is distinct from IAM access keys. Go to AWS Account Settings, find CloudFront Key Pairs, create a new pair, download the private key, and note the key pair ID. The private key is a .pem file that you’ll reference in your WordPress configuration.

Store sensitive values as constants in wp-config.php, not as WordPress options, so they’re never visible in the WordPress admin or exportable via database dumps:

In your wp-config.php file, add three constants: EDDSELL_CF_DOMAIN for your CloudFront distribution domain (e.g., d1234abcd.cloudfront.net), EDDSELL_CF_KEY_PAIR_ID for the key pair ID from AWS, and EDDSELL_CF_PRIVATE_KEY for the RSA private key contents as a string. The private key constant should contain the full PEM-formatted key including the -----BEGIN RSA PRIVATE KEY----- header and footer.

Alternative CDN providers

The CloudFront example uses AWS-specific signed URL logic, but the pattern works with any CDN that supports signed URLs. Bunny.net (formerly BunnyCDN) is popular for WordPress stores because of its simpler pricing model – you pay for bandwidth only, with no per-request fees. Bunny CDN’s signed URL system uses HMAC-SHA256 with an expiry timestamp, similar to what’s shown here but with a different URL format. Cloudflare Workers can also implement signed URL validation if you’re already using Cloudflare for your DNS.

CDN Provider Pricing Model Signed URL Support Best For
AWS CloudFront Requests + bandwidth Yes (RSA key pair) High scale, AWS ecosystem
Bunny.net Bandwidth only Yes (HMAC token) Budget-conscious stores
Cloudflare Free tier + pro plans Via Workers Already on Cloudflare
KeyCDN Bandwidth only Yes (token auth) Simple setup

S3 bucket setup for private file storage

For the CDN integration to work securely, your S3 bucket needs to block all public access. In the S3 console, enable “Block all public access” for the bucket. Create a CloudFront Origin Access Identity (OAI) or Origin Access Control (OAC) in CloudFront, then add a bucket policy that allows only the CloudFront distribution to read objects. This means files can never be accessed directly via S3 URLs – only via CloudFront signed URLs that your PHP code generates.

When you upload files for EDD products, store them in S3 rather than the WordPress uploads folder. The file URL you enter in the EDD product settings can be the S3 object key (path) rather than a full URL – your CDN delivery filter translates this to a signed CloudFront URL when the customer initiates a download.


Custom Download Templates

The purchase receipt page and email are the last touchpoint before a customer leaves your checkout flow. EDD’s default receipt is functional but generic – it lists the products purchased and shows a link to download files. For products that warrant a richer post-purchase experience – onboarding instructions, quick start guides, support links, upsells for related products – a custom template gives you full control.

EDD’s template system mirrors WordPress’s theme hierarchy. When EDD needs to render a template file, it searches through a list of template paths in priority order. By adding your theme directory to this list at a higher priority than EDD’s default path, your template files override EDD’s equivalents when they exist.

Template file structure

EDD looks for template files in your theme’s edd-templates/ directory (based on what you register in edd_template_paths). The main template files you can override are:

  • receipt.php – the purchase receipt page rendered by the [edd_receipt] shortcode
  • email/header.php – email template header
  • email/footer.php – email template footer
  • email/body.php – email body content
  • payment-history.php – the customer’s purchase history page
  • shortcodes/profile-editor.php – the customer profile edit form

Copy the original file from wp-content/plugins/easy-digital-downloads/templates/ to your theme’s edd-templates/ folder as a starting point, then modify from there. This is safer than writing templates from scratch because EDD’s templates use hooks and function calls that are easy to miss.

Product-specific download experiences

The custom shortcode in the code above demonstrates a pattern where the post-purchase page renders differently based on what was purchased. A software license product shows an API key. A file-based product shows download buttons. A course product could redirect to an enrollment page.

This is more maintainable than maintaining separate receipt templates per product type. The logic lives in PHP, the template is just a container, and adding a new product type means adding a new delivery method constant and a new branch in the display logic.

For email receipts specifically, EDD provides its own set of template tags that you can add to via the edd_email_tags filter. Rather than overriding the entire email template, you can add a custom tag like {api_key} and use it in the EDD email template editor in the admin. This keeps the template customization in the admin UI where your clients can manage it without touching PHP files.


Registering a Custom Delivery Method in the Admin

All the delivery methods above are controlled by a _edd_delivery_method post meta field. But you still need a way to set that field per product in the WordPress admin. EDD provides hooks for adding custom fields to the download settings metabox.

Add the delivery method selector to the EDD product settings using the edd_meta_box_settings_fields action. This action fires inside the EDD product settings metabox, so your field appears alongside the native EDD settings like price and file uploads. Pair this with a save_post hook (or edd_save_download if available) to persist the field value.

The field registration code is straightforward – a select dropdown with options for default, streaming, custom_cdn, api_credentials, and token_limited. Wrap it in a <p> tag with the EDD metabox styling classes to match the native admin appearance. On save, use update_post_meta() after running sanitize_text_field() and verifying against your allowed values array.


Combining Delivery Methods

Real EDD stores often need delivery methods that combine multiple approaches. A software license product might need CDN delivery for the installer file plus API credential delivery for the license key. A course product might need streaming delivery for preview content plus token-limited downloads for the full course files. Stores offering subscriptions or membership access often combine all of the above – see how EDD membership sites handle recurring revenue for context on how the payment layer fits into these more complex delivery workflows.

The single _edd_delivery_method meta field approach works for most products, but for complex products you can use a more granular approach: store the delivery method per file rather than per product. EDD’s file data is stored as a serialized array in the edd_download_files post meta. Each file entry is an array with keys like file, name, condition, and thumbnail_size. You can add a custom delivery_method key per file entry and read it in your delivery hooks.

This per-file approach requires custom admin fields in the EDD file row, which is more complex to implement but gives you fine-grained control. The EDD file row template is in easy-digital-downloads/includes/admin/downloads/metabox.php and uses the edd_download_file_table_header, edd_download_file_table_row, and related hooks for extension.


Testing Your Custom Delivery Methods

Custom delivery code needs testing beyond just “it works once manually.” Several scenarios require deliberate testing before going live:

  • Download limits – Create a product with a download limit of 1, make a purchase, download the file, then try to download again. The second attempt should hit EDD’s limit check, not your custom delivery handler
  • Expired purchases – EDD has a configurable download expiry. Test that an expired purchase key doesn’t bypass your delivery logic
  • Invalid payment keys – Pass a garbage payment key in the URL. Your handler should never receive this because EDD’s verification runs first, but confirm this in testing
  • Concurrent downloads – For single-use tokens, open two browser windows and click the download link simultaneously. One should succeed; the other should get an error
  • Large file streaming – Test with files larger than your PHP memory limit. The chunked read approach in the streaming code should handle this without memory issues
  • CDN URL expiry – Generate a signed CDN URL with a very short expiry (30 seconds), wait for it to expire, then try to access it. Confirm the CDN rejects it with a 403

Use EDD’s test mode for payment testing. In EDD settings, enable test mode, then use the test credit card numbers to complete purchases without real charges. Your custom delivery code runs the same way in test mode as in live mode, so this is a reliable testing environment.


Performance Considerations

Custom delivery methods add code that runs in the critical path of a customer’s download. A few things to watch for:

PHP streaming and memory

The chunked streaming approach in this article reads files in 8KB pieces. PHP’s memory usage stays low regardless of file size. But PHP’s execution time limit still applies to the total time taken to stream the file. On a slow connection, a 100MB file at 1Mbps takes over 13 minutes to transfer. Make sure your server’s max_execution_time is set appropriately for your expected file sizes and customer connection speeds, or use the set_time_limit(0) call shown in the streaming code.

Database lookups in delivery hooks

Every call to get_post_meta() in your delivery hook is a database query. For the delivery method check that routes to your custom code, this is unavoidable – you need to know which method to use. But avoid additional meta lookups inside the delivery handler where possible. Cache values from edd_get_payment() calls since the payment object is built from several queries.

CDN URL generation time

Generating signed CloudFront URLs involves an RSA cryptographic operation. This is fast on modern hardware (under 1 millisecond typically) but adds up if you’re generating many URLs in a loop. For a product with 10 files, generating 10 signed URLs is fine. For a product with 100 files, consider caching the signed URLs in a transient with a lifetime slightly shorter than the URL expiry window.


What’s Next in This Series

This article covered the delivery side of EDD customization – what happens after a purchase is complete and the customer initiates a download. The next article in this series will look at EDD’s customer management hooks, covering custom customer fields, customer dashboard customization, and building account-level features like download history exports and bulk license management.

If you’re building a store that uses one of the delivery patterns here and need help with the implementation, the EDD custom development services at EDD Sell Services cover the full stack – from initial architecture through deployment and ongoing maintenance. The contact form is the fastest way to get a project scoped.


Summary

EDD’s hook system gives you several clean extension points for replacing or augmenting its default file delivery. The approach throughout this article follows a single principle: hook early, check your routing meta field, handle the delivery if it matches, exit so EDD’s default doesn’t run, and return early when it doesn’t match so EDD proceeds normally.

  • Delivery hook architecture – Use edd_process_verified_download at priority 5 to intercept after EDD’s security checks pass
  • Streaming delivery – PHP chunk-based file streaming hides real file URLs and works for files stored outside the public web root
  • API credentials – Use edd_complete_purchase to generate credentials, store in payment meta, and filter edd_receipt_show_download_files to replace file links with credential display
  • Time-limited tokens – HMAC-signed payloads with embedded expiry timestamps create verifiable, stateless download tokens
  • CDN integration – Filter edd_download_file_url to replace plain URLs with signed CDN URLs; files stay private on S3 while transfers happen via edge nodes
  • Custom templates – Register a template path via edd_template_paths and use a custom shortcode for product-specific post-purchase experiences

All six code examples are available in the GitHub Gist linked throughout this article. Each snippet is self-contained and documented – you can drop individual pieces into a custom plugin or your theme’s functions.php, depending on how you prefer to manage site-specific code in EDD projects.

Leave a Comment

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

Scroll to Top