Getting StartedBuild an Add-on

Build an add-on

Connect a third-party WordPress plugin to PayLexer so its orders, refunds and subscriptions run through the merchant’s existing gateways.

You can take payments through PayLexer inside any third-party WordPress plugin by building an add-on: a separate plugin that handles the integration, so the plugin you integrate stays untouched. This page builds one step by step.

Throughout this page, replace the placeholders with the plugin you integrate:

PlaceholderMeaningExample shape
<plugin-slug>The integrated plugin's folder and platform idacme-forms
<Plugin Name>Its display nameAcme Forms
<Plugin>The name used in your class namesAcme_Forms

You'll need:

  • WordPress 6.5+, PHP 8.1+.
  • The PayLexer plugin 2.0.7 or later, connected, with at least one gateway enabled.
  • The plugin you're integrating, and to know where it lets other plugins add a payment method.

1. Start from the template

With Composer

cd wp-content/plugins
composer create-project paylexer/wordpress-starter paylexer-for-<plugin-slug>

With WP-CLI

Runs the same Composer command, so Composer must be installed, and skips the prompts.

wp package install paylexer/wp-cli-paylexer
wp paylexer scaffold paylexer-for-<plugin-slug> --platform=<plugin-slug> --platform-name="<Plugin Name>" --activate
FlagDefault
--platform, --platform-nameThe platform you integrate: its id and display name.
--nameThe slug in Title Case.
--prefixThe slug with underscores.
--namespaceThe slug in PascalCase.
--dirThe plugins directory.
--activateOff. Activates the plugin when it's ready.

With Composer, answer the prompts with your plugin's name and the platform you integrate (<plugin-slug> / <Plugin Name>). You get:

FileHolds
paylexer-for-<plugin-slug>.phpThe plugin header, the paylexer_init hook that registers your adapter, and an admin notice shown while PayLexer is missing.
src/Adapter.phpYour platform adapter: the platform's id, name and features, and on_event() for webhooks.
src/Payments.php, src/Admin.phpA sample pay shortcode and a settings page. Keep what helps, delete the rest.

Add the integrated plugin to the header so WordPress won't activate yours without it:

 * Requires Plugins: <plugin-slug>

If the integrated plugin isn't on WordPress.org (a paid plugin such as Gravity Forms), leave Requires Plugins out and show your own admin notice while it's missing: WordPress can only enforce dependencies it can install. Don't list paylexer there. Your addon should activate even when PayLexer isn't installed yet, then tell the admin what's missing. The template already does this on plugins_loaded:

add_action( 'plugins_loaded', function () {
	if ( function_exists( 'paylexer_register_adapter' ) ) {
		return; // PayLexer 2.0.7+ is active.
	}
	add_action( 'admin_notices', function () {
		if ( ! current_user_can( 'activate_plugins' ) ) {
			return;
		}
		$installed = file_exists( WP_PLUGIN_DIR . '/paylexer/paylexer.php' );
		$url       = $installed
			? wp_nonce_url( admin_url( 'plugins.php?action=activate&plugin=paylexer/paylexer.php' ), 'activate-plugin_paylexer/paylexer.php' )
			: admin_url( 'plugin-install.php?s=paylexer&tab=search&type=term' );
		printf(
			'<div class="notice notice-error"><p>%s <a href="%s">%s</a></p></div>',
			esc_html__( 'PayLexer for <Plugin Name> needs the PayLexer plugin (version 2.0.7 or later) installed and activated.', 'paylexer-for-<plugin-slug>' ),
			esc_url( $url ),
			$installed ? esc_html__( 'Activate PayLexer', 'paylexer-for-<plugin-slug>' ) : esc_html__( 'Install PayLexer', 'paylexer-for-<plugin-slug>' )
		);
	} );
} );

Everything that uses PayLexer still loads inside paylexer_init, which only fires while PayLexer is active, so the addon stays idle until then.

2. Describe the platform

In src/Adapter.php:

protected string $id          = '<plugin-slug>';
protected string $name        = '<Plugin Name>';
protected string $by          = 'PayLexer for <Plugin Name>';
protected string $plugin      = '<plugin-slug>/<plugin-slug>.php';
protected string $plugin_url  = 'https://wordpress.org/plugins/<plugin-slug>/';   // any other URL (a paid plugin's pricing page) shows a "Get" link
protected string $description = 'Payments in <Plugin Name>.';
protected array  $supports    = array( 'payments' );

While your addon is active, its platform can take payments through PayLexer. No setting has to be turned on. Register the adapter even while the integrated plugin is inactive, so PayLexer → General lists your addon with an Install, Activate or Get button. That means src/Adapter.php must load without the integrated plugin's classes; on_event() can throw until it's back, and PayLexer retries.

3. Find the integrated plugin's payment hook

Every plugin that takes payments has some way to register a payment method, usually a filter or a gateway base class. Read its code or docs for it. Add each gateway the merchant enabled in PayLexer as its own entry, the way PayLexer's built-in WooCommerce and WP Travel Engine integrations list them:

// Replace with the integrated plugin's own filter; find its name in that plugin's code or docs.
add_filter( '<plugin_slug>_register_payment_gateways', function ( array $gateways ): array {
	if ( ! function_exists( 'paylexer_register_adapter' ) || ! paylexer_is_connected() ) {
		return $gateways;
	}
	require_once __DIR__ . '/includes/class-gateway.php';
	foreach ( paylexer_get_gateways( array( 'test_mode' => true ) ) as $gateway ) {
		// "Dodo Payments", "Stripe", …, each extends the plugin's gateway base class.
		$gateways[ 'paylexer_' . $gateway['code'] ] = new PayLexer_<Plugin>_Gateway( $gateway['code'], $gateway['label'] );
	}
	return $gateways;
} );

The plugin's filter may run before paylexer_init, so check that PayLexer is there before loading anything that uses it.

4. Start the checkout

When the shopper submits, build a CheckoutRequest from the plugin's data and send the shopper to PayLexer's hosted checkout:

use PayLexer\Integration\CheckoutRequest;
use PayLexer\Integration\Customer;
use PayLexer\Integration\Exception as PayLexerException;

// Called by the integrated plugin when the shopper pays with this gateway.
public function process( $record_id, $total, $currency, $email, $first_name ) {
	try {
		$checkout = PayLexer_<Plugin>_Adapter::instance()->create_checkout( new CheckoutRequest(
			gateway: $this->code,                                // this gateway's PayLexer code
			amount: (float) $total,                              // computed by the plugin, never typed by the shopper
			currency: $currency,
			customer: new Customer( $email, $first_name ),
			return_url: wp_get_referer() ?: home_url( '/' ),   // wp_get_referer() only returns allowed hosts
			reference: "<plugin-slug>-{$record_id}",
			order_url: admin_url( "post.php?post={$record_id}&action=edit" ),   // the dashboard links the Purchase ID here
		) );
	} catch ( PayLexerException $e ) {
		return new WP_Error( 'paylexer', $e->getMessage() );   // show it the plugin's way
	}

	update_post_meta( $record_id, '_paylexer_order_reference', $checkout['order_reference'] );
	return $checkout['checkout_url'];   // redirect the shopper here, the plugin's way
}
  • Never take the amount from the shopper. Use a total the plugin computes (a fixed price or a calculation), never a field the shopper can type into: whatever it resolves to is what PayLexer charges.
  • Save order_reference on the integrated plugin's record (an order, booking or submission). Webhooks identify the order by it.
  • Redirect to checkout_url. It works for every gateway. If the plugin lets you run your own JavaScript after submit, pass the whole array to PayLexer.openCheckout() for the in-page modal instead.
  • One payment method per gateway. Register each gateway from gateways() as its own method in the plugin's list, like PayLexer's built-in WooCommerce and WP Travel Engine integrations, never one "PayLexer" method with a gateway picker.

5. Settle from the webhook

public function on_event( WebhookEvent $event ): void {
	$statuses = array( 'payment.completed' => 'paid', 'payment.failed' => 'failed', 'payment.cancelled' => 'cancelled', 'payment.refunded' => 'refunded' );
	$ignore_after = array( 'paid' => array( 'failed', 'cancelled' ), 'refunded' => array( 'paid', 'failed', 'cancelled' ) );
	$records = get_posts( array(
		'post_type'  => '<record_post_type>', 'post_status' => 'any', 'fields' => 'ids', 'numberposts' => 1,
		'meta_key'   => '_paylexer_order_reference', 'meta_value' => $event->order_reference,
	) );
	$status = $statuses[ $event->type ] ?? null;
	if ( ! $records || ! $status ) {
		return;
	}
	$current = (string) get_post_meta( $records[0], '_paylexer_status', true );
	if ( ! in_array( $status, $ignore_after[ $current ] ?? array(), true ) ) {
		update_post_meta( $records[0], '_paylexer_status', $status );
	}
}

Mark records paid from the webhook.

Events can arrive late or out of order. Never apply failed or cancelled to a paid or refunded record, and never apply paid to a refunded one.

  • Check on return. When the shopper comes back, the return URL proves nothing, but you can ask PayLexer: if the record is still pending, get_payment_status( $reference )['paid'] re-checks with the gateway, so the shopper sees "paid" even before the webhook arrives.
  • Sign the return link. If you do this on return, sign the link (for example a record id plus an HMAC key) so nobody can probe other records.
  • Lock the read-then-write. Two deliveries for one order can run at the same time, so take a lock (for example MySQL GET_LOCK() keyed on the record) and keep the processed event_ids on the record to skip repeats.

See Webhooks for retries and ordering.

If the integrated plugin sends emails or runs follow-up actions on submit or checkout (order confirmations, access grants, other add-ons' feeds), hold them until the record is paid and release them from on_event(), or shoppers get them for payments that never complete.

6. Refund from the integrated plugin

If the integrated plugin can refund (a refund button, a "refunded" status, a refund hook), hook it and call refund(). Otherwise a refund in its admin changes nothing at the gateway, and PayLexer never hears of it. If the plugin has no refunds, skip this step.

// Replace with the integrated plugin's own refund hook; find its name in that plugin's code or docs.
add_action( '<plugin_slug>_order_refunded', function ( $record_id, $amount ) {
	try {
		$reference = (string) get_post_meta( $record_id, '_paylexer_order_reference', true );
		PayLexer_<Plugin>_Adapter::instance()->refund( $reference, $amount, 'Refunded in <Plugin Name>' );
	} catch ( PayLexer\Integration\Exception $e ) {
		// Show it the plugin's way: PayLexer refused the refund.
	}
}, 10, 2 );

Declare 'refunds' in the adapter's $supports, or the refund is refused with paylexer_feature_not_declared. PayLexer confirms with a payment.refunded webhook.

7. Show the PayLexer Details box

Put the same box WooCommerce and EDD orders have (customer, gateway, transaction and PayLexer order, each linked) on the plugin's order or record screen. If the screen has two columns, it goes in the narrower right-hand sidebar (here, a side meta box):

add_action( 'add_meta_boxes_<record_post_type>', function ( WP_Post $post ) {
	$reference = (string) get_post_meta( $post->ID, '_paylexer_order_reference', true );
	if ( $reference ) {
		add_meta_box( 'paylexer-details', 'PayLexer Details', fn() => PayLexer_<Plugin>_Adapter::instance()->render_order_details( $reference, false ), '<record_post_type>', 'side' );
	}
} );

The box only renders for orders your platform created. The transaction fills in when PayLexer's webhook arrives.

8. Test it locally

Activate the plugins

Activate PayLexer, the integrated plugin and your addon.

Set up a payment

Set up a payment the integrated plugin's way, choosing a PayLexer gateway as the payment method.

Pay in the sandbox

Pay with a gateway's sandbox card. You're sent back, and the webhook marks the record paid. PayLexer → Webhook logs shows the delivery.

Check the details box

Open the order or record in the plugin's admin: the PayLexer Details box shows the gateway and, after the webhook, the transaction.

Refund it

Refund it from the plugin's admin: PayLexer's dashboard shows the refund.

Deactivate your addon

Deactivate your addon: the payment option disappears, and webhooks for its orders answer 503 until it's back.