Building a custom payment gateway for Easy Digital Downloads gives you full control over how your store processes payments. Whether you are integrating a regional payment provider that EDD does not support out of the box, building a white-label checkout for a client, or experimenting with a new payment API, understanding the full gateway stack from registration to webhook handling is the foundation you need. This tutorial walks through the complete custom EDD payment gateway development process: class structure, payment processing, IPN and webhook handling, refund implementation, settings registration, and sandbox testing – all with production-ready PHP code.
Prerequisites and Environment Setup
Before diving into the code, make sure your development environment and knowledge base are aligned with what this tutorial assumes. Gateway development sits at the intersection of WordPress plugin development, REST API consumption, and payment processing – getting any of the setup wrong will cost you time later.
| Requirement | Minimum Version | Notes |
|---|---|---|
| Easy Digital Downloads | 3.0+ | EDD 3.0 rewrote the Orders system. Some hooks differ from EDD 2.x. |
| PHP | 8.0+ | Code uses typed properties and named arguments. PHP 7.4 needs minor adjustments. |
| WordPress | 6.0+ | For modern Gutenberg admin compatibility in settings screens. |
| Payment Provider Account | Sandbox access | You need API keys and a webhook secret before writing any code. |
| ngrok or similar tunnel | Any | Required for testing webhooks against a local development site. |
You should be comfortable writing WordPress plugins (hooks, filters, the options API, custom post meta) and have a basic understanding of how HTTP APIs work (request/response, headers, status codes, JSON). You do not need to know anything specific about EDD internals – this tutorial covers everything EDD-specific as we go.
EDD 2.x vs EDD 3.x – What Changed
If you are supporting a site still running EDD 2.x, be aware of two important differences. EDD 2.x uses edd_insert_payment() and the EDD_Payment class for order management. EDD 3.x introduced a dedicated custom database table for orders (wp_edd_orders) and prefers edd_add_order(), though it keeps the older functions as wrappers for backward compatibility. This tutorial targets EDD 3.x. If you need 2.x support, the gateway registration and hook names are identical – only the order management functions differ.
What the EDD Payment Gateway API Does
EDD uses a filter-and-action-based gateway API rather than an abstract class system. That makes it flexible but also means there is no single interface to implement – you register a gateway via a filter, attach actions for the checkout form and payment processing, and optionally hook into refunds and settings. EDD handles the cart, order creation scaffolding, and success/failure routing. Your gateway handles the actual API calls.
Here is what you are responsible for as a gateway developer:
- Registering the gateway so EDD includes it in the checkout options list
- Rendering any checkout fields your payment form needs (card token input, redirect button, etc.)
- Receiving the purchase data, creating a pending order, calling your API, and updating the order status
- Handling async payment notifications (IPN/webhooks) from the provider
- Processing refunds when an admin clicks “Refund” in the EDD Orders screen
- Adding settings fields for API keys and configuration
EDD does not care what happens inside your gateway as long as it calls the right status update functions at the end. That gives you a lot of room to adapt to whatever API shape your provider uses.
Step 1 – Gateway Class Structure and Registration
Start by building the class shell that wires all the hooks together. EDD’s gateway system is entirely hook-driven, so the constructor is where you bind each part of the gateway to the right EDD action or filter.
A few things to get right in the structure before writing any payment logic:
- Gateway ID must be unique – EDD uses it as an array key and as part of hook names. A collision with another gateway (including built-in ones like PayPal or Stripe) will cause silent failures.
- Instantiate on
plugins_loaded– notinit. EDD itself loads onplugins_loadedat priority 10. Using priority 11 or later guaranteesEDD()exists when you check for it. - Check for EDD before instantiating – wrap your instantiation in a
function_exists( 'EDD' )check so the plugin degrades gracefully if EDD is deactivated.
The register_gateway filter adds your gateway to EDD’s internal registry. The admin_label appears in the EDD settings screen; the checkout_label is what buyers see. The supports array can include buy_now (direct purchase without cart) and recurring (for EDD Recurring Payments add-on compatibility).
Step 2 – Payment Processing Flow
The payment processing method is the core of your gateway. EDD calls it via a dynamic action: edd_gateway_{your_gateway_id}. It receives a $purchase_data array that contains everything EDD knows about the order – buyer details, cart items, pricing, and any custom fields from your checkout form.
The processing flow follows a strict sequence and every step matters:
| Step | Action | Why It Matters |
|---|---|---|
| 1 | Verify nonce | Prevents CSRF attacks on the checkout form |
| 2 | Check existing errors | EDD may have already caught field validation errors |
| 3 | Create pending payment | Establishes the order record before touching the API |
| 4 | Call provider API | Charge the card or initiate the payment flow |
| 5 | Handle API response | Update order status to complete or failed |
| 6 | Redirect buyer | Send to success page or back to checkout on error |
Creating the pending payment record in step 3 before calling the API is important. If your API call succeeds but your server crashes before you update the status, you now have a record to reconcile against. Without it, you have a charged card and no order in EDD.
Always create the EDD payment record before calling the provider API. A pending order you can fix is better than a charge with no corresponding record.
Notice that amounts are passed to the API in the smallest currency unit (cents for USD). EDD stores prices as decimals. Always multiply by 100 and round before sending to your provider – most APIs require integer amounts and will reject decimal values.
Checkout Form Fields
If your gateway handles card data directly on-site, EDD calls edd_{gateway_id}_cc_form to render your fields. Most modern integrations use a JavaScript SDK that tokenizes the card in the browser before the form submits – your PHP never touches raw card data. In that case, render a hidden input for the token and a placeholder div where the JS SDK mounts its card UI:
For PCI compliance, always tokenize card data in the browser using your provider’s JS SDK. Never pass raw card numbers through your server.
If your gateway redirects the buyer to a hosted payment page (like PayPal Standard), the cc_form action should render nothing – just output a “You will be redirected…” message and handle the redirect inside process_payment.
Step 3 – IPN and Webhook Handling
Webhooks are how your payment provider tells you about events that happen after the browser session ends – payment confirmations, failures, chargebacks, and refunds. Relying only on the synchronous API response in process_payment is not enough: the response can timeout, the buyer can close their browser before the success redirect fires, or the provider may process the payment asynchronously.
EDD has a built-in listener URL pattern for gateways: /?edd-listener={gateway_id}. Your provider should be configured to POST events to this URL. The init hook fires early enough to intercept the request and respond before WordPress renders any output. If you are also using no-code tools to trigger workflows on sales, see the guide on EDD webhook automation with Zapier, Make, and n8n for the complementary setup.
Webhook Security – Signature Verification
Never process a webhook without verifying its signature. Without verification, any HTTP request to your listener URL could forge a payment confirmation. The standard approach is HMAC-SHA256: the provider signs the raw request body with a shared secret, sends the signature in a header, and you verify it server-side before trusting any data in the payload.
Key rules for webhook handling:
- Always verify the signature first, before parsing the event type or looking up any database records
- Use the raw request body for signature verification – parsing the JSON first can alter whitespace and break the HMAC
- Return 200 for unknown event types – if you return 4xx, the provider will keep retrying, filling your logs
- Handle duplicates idempotently – providers may send the same event more than once. Check the current order status before updating it
- Respond quickly – most providers expect a response within 5-10 seconds. Do heavy processing in a background job if needed
Step 4 – Refund Implementation
EDD 3.0 introduced a dedicated refund UI in the Orders screen. When an admin clicks “Refund”, EDD fires the edd_should_process_refund filter before changing the order status. Your gateway hooks into that filter to send the refund request to the provider API before EDD marks the order as refunded.
If the API call fails, return false from the filter. EDD will not change the status, and the admin sees the order still in its previous state. This prevents a refund from being recorded in EDD without actually being processed by the payment provider.
Store the refund ID returned by the provider in order meta. You will need it for auditing, support queries, and chargeback documentation. Use edd_add_note to attach a human-readable note to the order – it shows in the order timeline in the EDD admin and is searchable.
Partial Refunds
EDD 3.0 supports partial refunds via the Refund Items screen. To support them in your gateway, check whether the refund amount passed to your API equals the original charge or a partial amount. Most provider APIs accept an optional amount parameter on the refund endpoint – if omitted, they issue a full refund. Pass the partial amount explicitly when EDD provides it via edd_get_payment_amount( $payment_id ) versus the refund line items total.
Step 5 – Gateway Settings Registration
EDD’s settings API lets you add your own fields to the Gateways tab without building a custom admin page. Use the edd_settings_gateways filter to inject your fields into the existing settings array.
A few practical points about settings:
- Use
type: 'password'for API keys so they are masked in the UI and not exposed in browser autofill - Include a test mode toggle so developers and site owners can switch between sandbox and live credentials without editing a config file
- Display the full webhook URL in the settings description so whoever sets up the provider account does not have to construct it manually
- Retrieve settings with
edd_get_option()– never access the raw option directly. It handles defaults and sanitization
Step 6 – Testing with Sandbox Environments
Every payment provider offers a sandbox mode with test credentials and test card numbers. Use it to validate every scenario before going live. A broken refund flow or a webhook that silently fails is far cheaper to discover in a sandbox than after real money has moved.
For local development, you need a publicly accessible URL for webhooks. Use ngrok (ngrok http 80) to tunnel your local site to a temporary public URL, then set that URL as your webhook endpoint in the provider’s sandbox dashboard. Remember to update the URL each time you restart ngrok unless you are on a paid plan with a static subdomain.
EDD’s Built-in Test Mode
EDD itself has a test mode under Settings > General. When test mode is on, EDD skips actual gateway processing and auto-completes purchases. This is useful for testing the download delivery flow, but it bypasses your gateway entirely. To test your gateway code, you need to use your provider’s sandbox with real test API calls – not EDD’s test mode. Keep EDD test mode off when testing your gateway.
You can detect EDD’s test mode in your gateway code with edd_is_test_mode() and switch between live and test API keys accordingly – but this is secondary to your own sandbox toggle in the gateway settings.
Putting It All Together – Plugin File Structure
Once you have the class built, organize it into a proper plugin so it can be activated independently from EDD and maintained separately. A minimal file structure looks like this:
my-edd-gateway/
– my-edd-gateway.php (main plugin file with headers)
– includes/
, class-edd-custom-gateway.php (the gateway class)
, class-edd-checkout-fields.php (optional: separate checkout form logic)
– assets/
, js/checkout.js (your provider’s JS SDK initialization)
The main plugin file should declare EDD as a dependency using the standard plugin header comment and check for EDD before loading any classes. If you ever intend to submit the gateway to the WordPress.org plugin directory, follow the Plugin Review Guidelines – EDD add-ons with proper dependency checks are generally accepted without issue.
Error Logging Best Practices
Use EDD’s gateway error logging rather than writing to error_log directly. edd_record_gateway_error() stores errors in the EDD error log, which is viewable in EDD > Reports > Logs > Gateway Errors. This is the first place a site owner will check when payments fail, and it is far more useful than digging through server error logs.
- Log every API error with the full error message and the EDD payment ID
- Add order notes with
edd_add_note()for significant events (payment confirmed, refund issued, webhook received) - Never log raw card data, API keys, or any PII in error logs or order notes
- Use descriptive error titles – “Payment API Error” is searchable, “Error” is not
EDD Recurring Payments Compatibility
If you want your gateway to work with the EDD Recurring Payments add-on, there is additional integration work beyond the base gateway. EDD Recurring requires your gateway to support subscription creation, renewal processing, and cancellation – three distinct flows that each map to different hooks.
To indicate that your gateway supports recurring payments, add 'recurring' to the supports array in your register_gateway method. EDD Recurring then checks for this flag and makes your gateway available when buyers are purchasing products with recurring pricing.
The key hooks for recurring support are:
edd_recurring_create_subscription_payment– fires when a renewal payment needs to be processed. Your gateway should charge the stored payment method and return the transaction ID.edd_recurring_cancel_{gateway_id}_subscription– fires when a subscriber cancels. Cancel the subscription in your provider’s system so it stops charging.edd_recurring_record_payment– called after a successful renewal to update the subscription status in EDD.
Recurring integration is significantly more complex than one-time payments because you are managing stored payment methods and subscription state across two systems simultaneously. It is best treated as a separate development phase from the base gateway. For a practical guide on setting up the store side, read the complete walkthrough on setting up recurring payments in EDD before implementing recurring support in your gateway.
Common Pitfalls to Avoid
Building a custom EDD payment gateway is straightforward once you understand the hook system, but there are a handful of mistakes that come up repeatedly and can cost hours of debugging:
- Calling
edd_empty_cart()before the API succeeds – if the API call fails after you clear the cart, the buyer has an empty cart and no order. Always clear the cart only after a successful charge. - Not handling the nonce on the webhook endpoint – the IPN listener does not need a nonce (the provider cannot provide one), but your checkout processing method does. Keep them separate.
- Using
wp_die()in a webhook handler –wp_die()outputs HTML. Your provider expects a plain text or empty 200 response. Usestatus_header()andexit()instead. - Forgetting that EDD’s “publish” status means “complete” – in standard WordPress terms, a published post is live content. In EDD,
publishis the order status for a successfully completed payment. This confuses developers coming from non-EDD WordPress backgrounds. - Updating order status on every webhook event without idempotency checks – if the provider sends
payment.succeededtwice (which happens more often than you would expect), updating the status twice can re-trigger download access emails and license key generation. - Hardcoding currency assumptions – not every EDD store runs in USD. Always use
edd_get_currency()and check whether your provider supports the store’s configured currency before processing. - Skipping the wp_remote_post timeout – the default WordPress HTTP timeout is 5 seconds. Payment provider APIs can take longer under load. Set
timeoutto 30 seconds explicitly in everywp_remote_postcall or you will see intermittent failures that are impossible to reproduce locally.
What is Next in This Series
This is Article 1 of 6 in the EDD Custom Development series. Custom payment gateways are often the first step toward building more complex platforms – once you have checkout handled, you can look at building a full SaaS subscription platform with Easy Digital Downloads. The next articles in this series cover:
- Article 1 (this post) – Custom Payment Gateway Development
- Article 2 – Building EDD Extensions and Add-ons from Scratch
- Article 3 – Custom Download Types and File Delivery Mechanisms
- Article 4 – EDD REST API: Custom Endpoints and Authentication
- Article 5 – Integrating EDD with External CRMs and Email Platforms
- Article 6 – Performance Optimization for High-Volume EDD Stores
Build Your EDD Gateway the Right Way
The EDD payment gateway API is well-designed once you understand its hook-driven pattern. The class structure in this tutorial gives you a solid starting point that covers every scenario your store will encounter in production: synchronous payment processing, asynchronous webhook handling, admin-initiated refunds, and a proper settings UI. The code snippets above are production-ready – copy them as your base and adapt the API calls to your specific provider’s endpoint format.
If you need help implementing a custom EDD payment gateway for your store or client project, the team at EDD Sell Services specializes in EDD custom development. We build gateways, extensions, and integrations that are properly tested, maintainable, and compatible with EDD’s update cycle.
Production Deployment Checklist
Before switching your gateway from sandbox to live mode, work through these verification steps to catch issues before real money is involved. Swap all API keys from sandbox to production credentials in EDD Settings, then update the webhook URL in your payment provider’s dashboard from your ngrok tunnel to your production domain URL. Run one test purchase with a real payment method for a small amount and refund it immediately afterward to confirm both the charge and refund flows work end to end. Verify that webhook signature verification passes correctly against your production webhook secret, and check that EDD’s gateway error log is clean after the test. This final round of production verification prevents launch-day payment failures that would have been trivial to catch during a quick staging run.
