This article provides a complete, copy-pasteable addon plugin example that registers a custom payment gateway called “AcmePay”. It demonstrates the full descriptor pattern and all required interface methods.
Plugin File Structure
wp-content/plugins/
my-acmepay-gateway/
my-acmepay-gateway.php ← main plugin file
class-acmepay.php ← gateway class
Main Plugin File
<?php
/**
* Plugin Name: AcmePay for Influenzic
* Description: Adds AcmePay as a payment gateway for the Influenzic platform.
* Version: 1.0.0
*/
defined( 'ABSPATH' ) || exit;
require_once __DIR__ . '/class-acmepay.php';
/**
* Register AcmePay with the Influenzic gateway registry.
* Fire at plugins_loaded priority 10 — registry filter runs at priority 20.
*/
add_filter( 'inzc_payment_gateways', function( array $gateways ): array {
$gateways['acmepay'] = [
'id' => 'acmepay',
'label' => __( 'AcmePay', 'my-acmepay-gateway' ),
'class' => AcmePay_Gateway::class,
'webhook_slug' => 'acmepay',
'redux_fields' => [
[
'id' => 'inzc_acmepay_api_key',
'type' => 'password',
'title' => 'AcmePay API Key',
'default' => '',
],
[
'id' => 'inzc_acmepay_webhook_secret',
'type' => 'password',
'title' => 'AcmePay Webhook Secret',
'default' => '',
],
],
];
return $gateways;
} );
Gateway Class
use InfluenzicApiGatewaysGateway_Interface;
class AcmePay_Gateway implements Gateway_Interface {
public function supported_currencies(): array {
return []; // Accepts all currencies
}
public function initiate( array $order_data ) {
$key = (string) inzc_option( 'inzc_acmepay_api_key', '' );
$currency = inzc_currency_code();
$total = $order_data['totals']['total'];
$batch_id = $order_data['batch_id'];
$cancel_url = $order_data['cancel_url']; // always use the provided cancel_url
$response = wp_remote_post( 'https://api.acmepay.example/checkout', [
'headers' => [
'Authorization' => 'Bearer ' . $key,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( [
'amount' => $total,
'currency' => $currency,
'reference' => $batch_id,
'cancel_url' => $cancel_url,
// Store inzc_payment_type in gateway metadata so the webhook
// can read it back and pass it to inzc_dispatch_payment_complete().
'metadata' => [
'inzc_payment_type' => $order_data['payment_type'],
'inzc_batch_id' => $batch_id,
'inzc_brand_id' => $order_data['brand_id'],
'inzc_subtotal' => $order_data['totals']['subtotal'],
'inzc_fee_pct' => $order_data['totals']['fee_pct'],
],
] ),
] );
if ( is_wp_error( $response ) ) {
return new WP_Error( 'acmepay_error', $response->get_error_message(), ['status' => 502] );
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( empty( $body['checkout_url'] ) ) {
return new WP_Error( 'acmepay_no_url', 'AcmePay did not return a checkout URL.', ['status' => 502] );
}
return [
'success' => true,
'gateway' => 'acmepay',
'redirect_url' => esc_url_raw( $body['checkout_url'] ),
'order_ref' => $batch_id,
];
}
public function verify( string $reference ): bool {
$key = (string) inzc_option( 'inzc_acmepay_api_key', '' );
$response = wp_remote_get( 'https://api.acmepay.example/charge/' . $reference, [
'headers' => [ 'Authorization' => 'Bearer ' . $key ],
] );
if ( is_wp_error( $response ) ) {
return false;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
return isset( $body['status'] ) && 'paid' === $body['status'];
}
public function handle_webhook( WP_REST_Request $request ): WP_REST_Response {
$payload = $request->get_json_params();
$signature = $request->get_header( 'X-AcmePay-Signature' );
$secret = (string) inzc_option( 'inzc_acmepay_webhook_secret', '' );
// Verify signature — reject forged events.
$expected = hash_hmac( 'sha256', $request->get_body(), $secret );
if ( ! hash_equals( $expected, (string) $signature ) ) {
return new WP_REST_Response( ['error' => 'invalid_signature'], 400 );
}
if ( ( $payload['status'] ?? '' ) !== 'paid' ) {
return new WP_REST_Response( ['received' => true], 200 );
}
$meta = $payload['metadata'] ?? [];
$totals = [
'subtotal' => (float) ( $meta['inzc_subtotal'] ?? 0 ),
'fee_pct' => (float) ( $meta['inzc_fee_pct'] ?? 0 ),
];
// Route to the correct payment handler — never call activate_invitations() directly.
inzc_dispatch_payment_complete( $meta['inzc_payment_type'] ?? 'invitation', $meta, $totals );
return new WP_REST_Response( ['received' => true], 200 );
}
// Subscription methods — return WP_Error to use the manual-renewal fallback.
public function charge_renewal( string $gw_sub_id, float $amount, array $context = [] ) {
return new WP_Error( 'gateway_manual_renewal', 'AcmePay does not support automatic renewals.' );
}
public function cancel_subscription( string $gw_sub_id ) {
return true; // Nothing to cancel on the gateway side.
}
}
Webhook URL auto-registered: Because
webhook_slug is acmepay, Influenzic automatically registers the endpoint at /wp-json/influenzic/v1/webhook/acmepay. Configure this URL in your AcmePay dashboard.
Always use
inzc_dispatch_payment_complete(). Never call activate_invitations() or any other payment-type handler directly from your webhook. The dispatcher routes to the correct handler based on the inzc_payment_type value stored in gateway metadata at initiate() time.