Local development and testing

Updated

Turn on Sandbox in the gateway settings and use sandbox credentials. Paddle’s sandbox test card is 4242 4242 4242 4242 with any future expiry date and any CVC. For Billing, approve the domain and set the default payment link in the sandbox account too, otherwise POST /transactions fails.

Webhooks on a local site

Paddle can’t reach *.local hosts, and Set up webhook refuses a non-https:// webhook URL. Expose the site with a tunnel:

cloudflared tunnel --url http://site.local --http-host-header site.local

Then point the webhook URL at it with a must-use plugin:

wp-content/mu-plugins/paddle-tunnel.php

<?php
add_filter( 'wpinterface_paddle_woo_webhook_base_url', function () {
	return 'https://<random>.trycloudflare.com/';
} );

Quick-tunnel URLs change on every start

Update the filter, then click Set up webhook again (Billing; it creates a new destination for the new URL) or update the Classic alert URL. Classic one-time pay links carry the current webhook URL themselves.

Logging

Plugin::log( $message, $routine = false ) writes to the WooCommerce logger with source wpinterface-paddle (WooCommerce → Status → Logs). Problems are always logged at error level; routine traffic (every API call, every received webhook) only with Debug logging on.

Simulating Billing webhooks

Sign the exact raw body with the sandbox webhook secret within 300 seconds:

ts=$(date +%s)
body='{"event_id":"evt_test","event_type":"transaction.completed","data":{"id":"txn_…","status":"completed","currency_code":"USD","custom_data":{"wc_order_id":123,"wc_site":"<site tag>"},"details":{"totals":{"total":"999","tax":"0","earnings":"900"},"line_items":[]}}}'
sig=$(printf '%s:%s' "$ts" "$body" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -X POST https://site/wp-json/wpinterface-paddle-woo/v1/webhook \
     -H "Paddle-Signature: ts=$ts;h1=$sig" \
     -H 'Content-Type: application/json' -d "$body"

Get the site tag with wp eval 'echo WPInterface\Paddle_Woo_Pro\Plugin::site_tag();'. The transaction ID must equal the order’s _wpinterface_paddle_transaction_id. Change event_id between runs, or the event is answered “Already processed”.

Mocking settings and the Paddle API

Inject settings with pre_option_woocommerce_wpinterface_paddle_settings and flush the request cache:

tests/bootstrap.php

add_filter( 'pre_option_woocommerce_wpinterface_paddle_settings', function () {
	return array(
		'enabled'                        => 'yes',
		'testmode'                       => 'yes',
		'api_mode'                       => 'billing',
		'billing_sandbox_api_key'        => 'pdl_sdbx_apikey_test',
		'billing_sandbox_client_token'   => 'test_0123456789',
		'billing_sandbox_webhook_secret' => 'pdl_ntfset_test',
	);
} );
\WPInterface\Paddle_Woo_Pro\Settings::flush();

Short-circuit HTTP with pre_http_request for sandbox-api.paddle.com / sandbox-vendors.paddle.com:

tests/bootstrap.php

add_filter( 'pre_http_request', function ( $pre, $args, $url ) {
	if ( 0 === strpos( $url, 'https://sandbox-api.paddle.com/transactions' ) && 'POST' === $args['method'] ) {
		return array(
			'headers'  => array(),
			'body'     => wp_json_encode( array( 'data' => array( 'id' => 'txn_test', 'items' => array() ) ) ),
			'response' => array( 'code' => 201, 'message' => 'Created' ),
			'cookies'  => array(),
			'filename' => null,
		);
	}
	return $pre;
}, 10, 3 );

Classic webhooks can’t be signed locally without Paddle’s private key. Unit-test Signature::verify_classic() with your own key pair, and call Plugin::instance()->driver_for_api( 'classic' )->handle_alert( $fields, true ) directly for alert logic. Signature::verify_billing() takes $now for deterministic tests.

Was this page helpful?