Hooks & Filters

Registering a Custom Gateway — Complete Example

A complete copy-pasteable plugin example showing gateway descriptor registration and a working Gateway_Interface implementation.

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.

Was this article helpful?