Test against Paddle’s sandbox with EDD Test Mode on. Paddle’s sandbox test card is 4242 4242 4242 4242 with any future expiry date and any CVC.
Webhooks on a local site
Paddle cannot reach *.local hosts, and Paddle Classic refuses to open a checkout whose webhook URL is unreachable. Expose the site with a tunnel:
cloudflared tunnel --url http://site.local --http-host-header site.localThen point webhooks at it with a must-use plugin:
wp-content/mu-plugins/paddle-tunnel.php
<?php
add_filter( 'wpinterface_paddle_webhook_base_url', function () {
return 'https://<random>.trycloudflare.com/';
} );Quick-tunnel URLs change on every start
Update the filter, the Classic alert URL, or click Set up webhook again (Billing).
Logging
Turn on Debug logging and watch Downloads → Tools → Debug Log (lines prefixed Paddle:). Problems are logged even with the setting off.
Simulating Billing webhooks
Sign the exact raw body with the sandbox webhook secret:
ts=$(date +%s)
body='{"event_id":"evt_test","event_type":"transaction.completed","data":{ … }}'
sig=$(printf '%s:%s' "$ts" "$body" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -X POST https://site/wp-json/wpinterface-paddle/v1/webhook \
-H "Paddle-Signature: ts=$ts;h1=$sig" \
-H 'Content-Type: application/json' -d "$body"Mocking the Paddle API
In PHP tests, short-circuit HTTP with pre_http_request for sandbox-api.paddle.com / sandbox-vendors.paddle.com, and inject fake credentials:
tests/bootstrap.php
add_filter( 'edd_get_option_paddle_billing_sandbox_api_key', function () {
return 'pdl_sdbx_apikey_test';
} );Classic webhooks cannot be signed locally without Paddle’s private key. Unit-test Signature::verify_classic() with your own key pair, and call Classic_Driver::handle_alert( $fields, true ) directly for alert logic.