Architecture

Updated

The plugin registers an EDD payment gateway with the ID paddle. It supports two Paddle platforms through interchangeable drivers.

Paddle BillingPaddle Classic
APIapi.paddle.com / sandbox-api.paddle.com (REST, JSON, Authorization: Bearer)vendors.paddle.com/api/2.0 / sandbox-vendors.paddle.com/api/2.0 (form POST, vendor_id + vendor_auth_code)
Browser libraryPaddle.js v2 (Paddle.Initialize, Paddle.Checkout.open({ transactionId }))Paddle.js v1 (Paddle.Setup, Paddle.Checkout.open({ override }))
What an order becomesA transaction created server-side with non-catalog itemsA pay link created by product/generate_pay_link
SubscriptionsRecurring non-catalog price in the transaction; Paddle creates the subscriptionAn auto-created Classic plan plus price overrides on the pay link
Webhook authenticationPaddle-Signature HMAC-SHA256p_signature RSA-SHA1

Design principles

  1. EDD is the source of truth for prices. Nothing has to exist in the Paddle catalog; Paddle charges exactly the EDD order total.
  2. Nothing from the browser or a webhook is trusted without verification. Orders are completed only by signed webhooks or by server-side API reads.
  3. Webhooks are processed idempotently and safely under concurrency. Paddle sends related events in parallel and retries freely.
  4. Every site on a Paddle account receives every webhook. Each site acts only on its own orders.
  5. Orders remember the API and mode they were created with, so refunds and subscription changes keep working after a store switches account type or goes live.

Requirements and compatibility

  • PHP 7.4+ (no PHP 8-only syntax). The OpenSSL extension is required for Paddle Classic signature checks.
  • WordPress 6.0+; Easy Digital Downloads 3.1+ (uses the EDD 3 order, refund and transaction APIs).
  • Optional: EDD Recurring Payments (tested 2.13.13), EDD Software Licensing (tested 3.8.14).
  • MySQL/MariaDB GET_LOCK() (MySQL 5.7+ allows nested locks).
  • The plugin header sets Update URI, so WordPress.org never offers an “update” for this plugin. There is intentionally no Requires Plugins header, so activation also works with EDD Pro.

Files

wpinterface-paddle-for-edd-pro/
├── wpinterface-paddle-for-edd-pro.php   Bootstrap: constants, requires, boots Plugin on plugins_loaded:20
├── uninstall.php                        Opt-in removal of settings and caches
├── includes/
│   ├── class-plugin.php                 Service container, requirements, logging, API factories
│   ├── class-settings.php               Settings section, credential lookup, panels
│   ├── class-gateway.php                Gateway registration, one-time purchases, checkout AJAX, success page
│   ├── class-recurring-gateway.php      EDD_Recurring_Gateway subclass (loaded only with EDD Recurring)
│   ├── class-orders.php                 Shared bookkeeping: completion, refunds, renewals, locks, ledger
│   ├── class-webhooks.php               REST endpoint, signature dispatch
│   ├── class-refunds.php                Admin refunds sent to Paddle
│   ├── class-customer.php               Invoice/receipt link, update-payment-method URL
│   ├── class-admin.php                  Order panel, links, notices, AJAX tools, pause/resume
│   ├── class-signature.php              Billing HMAC and Classic RSA verification
│   ├── api/                             API_Exception, Billing_API, Classic_API
│   └── drivers/                         Driver (abstract), Billing_Driver, Classic_Driver
├── assets/                              paddle-checkout.js, admin.js, checkout.css, payment marks
└── languages/                           Translation template

Namespace: WPInterface\Paddle_EDD_Pro (drivers in \Drivers, API clients in \API). Constants: WPINTERFACE_PADDLE_PRO_VERSION, WPINTERFACE_PADDLE_PRO_FILE, WPINTERFACE_PADDLE_PRO_DIR, WPINTERFACE_PADDLE_PRO_URL.

Boot sequence

WhenWhat
Plugin file loadAll classes are required (no autoloader).
plugins_loaded priority 20If EDD() exists, Plugin::instance() is created. Otherwise an admin notice is shown.
Plugin::__constructCreates Settings. If the requirements pass (EDD 3.1 functions; OpenSSL for Classic), creates Gateway, Webhooks, Refunds, Customer and, in admin, Admin.
initText domain loaded (wpinterface-paddle-for-edd-pro).
plugins_loaded priority 99If EDD_Recurring_Gateway exists (EDD Recurring boots at 98), Recurring_Gateway is instantiated and returned from the edd_recurring_gateway filter for the ID paddle.

One Recurring_Gateway instance

Plugin::instance()->recurring must be the only Recurring_Gateway instance. EDD Recurring’s own get_gateway() would otherwise construct new instances, each registering its hooks again. That is why the filter returns the singleton and paddle is not added to EDD Recurring’s gateway map.

Drivers

Plugin::driver( $order = null ) returns the driver for an existing order (by its _wpinterface_paddle_api meta) or, for new orders, the configured API (Settings::api_mode()). Orders without the meta are treated as Classic. Plugin::driver_for_api( 'billing'|'classic' ) returns a specific driver.

MethodPurpose
api()'billing' or 'classic'
start_checkout( $order, $purchase_data, $subscription )Create the Paddle transaction or pay link. Returns transaction_id or pay_link, plus redirect_url.
refund( $order, $amount, $full, $reason, $refund = null )Refund in Paddle (amount in EDD terms).
cancel_subscription( $subscription, $immediately )Cancel in Paddle; “already cancelled” counts as success.
set_paused( $subscription, $paused )Pause at period end, or resume.
next_billing_date( $subscription )Site-local next bill date (used by EDD Recurring’s expiration check).
test_connection( $sandbox )Credential check with readable errors.
transaction_url(), subscription_url()Paddle dashboard links.
sync_order( $order )Billing only: read the transaction from the API and complete the order if paid.
one_time_lines()Shared: turns the EDD cart into charge lines and the reconciliation amount.

API clients

API\Billing_API::request( $method, $path, $body = null, $query = array() ) returns the decoded data member, or throws API_Exception (HTTP status as the code; Paddle’s error.code via get_paddle_code()). API\Classic_API::request( $action, $fields ) returns the response member, or throws when success is false. Both log every call and are created through Plugin::billing_api( $sandbox ) / Plugin::classic_api( $sandbox ), which pick the credentials of the requested environment.

Was this page helpful?