Building Custom WooCommerce Payment Gateways
Payment gateways are the backbone of any e-commerce operation, and WooCommerce's extensible architecture makes it possible to integrate virtually any payment provider. This comprehensive guide walks you through creating professional payment gateways, from basic implementations to advanced features like tokenization, subscriptions, and multi-currency support.
Whether you're integrating a regional payment provider or building a custom solution, understanding WooCommerce's payment gateway framework is crucial. We'll cover security best practices, PCI compliance, testing strategies, and real-world implementation patterns that ensure your payment gateway is both robust and user-friendly.
Table of Contents
- Payment Gateway Architecture
- Building Your First Gateway
- Advanced Payment Features
- Security and Compliance
- Testing and Debugging
- Integration Best Practices
Payment Gateway Architecture
Understanding WooCommerce Payment Flow
// Payment gateway base implementation
abstract class WC_Payment_Gateway_Base extends WC_Payment_Gateway {
protected $api_endpoint;
protected $api_key;
protected $api_secret;
protected $webhook_secret;
protected $test_mode;
/**
* Gateway initialization
*/
public function __construct() {
$this->init_form_fields();
$this->init_settings();
// Define gateway properties
$this->title = $this->get_option('title');
$this->description = $this->get_option('description');
$this->enabled = $this->get_option('enabled');
$this->test_mode = 'yes' === $this->get_option('testmode');
// API configuration
$this->api_endpoint = $this->test_mode
? 'https://api-sandbox.payment-provider.com/v1/'
: 'https://api.payment-provider.com/v1/';
$this->api_key = $this->test_mode
? $this->get_option('test_api_key')
: $this->get_option('live_api_key');
$this->api_secret = $this->test_mode
? $this->get_option('test_api_secret')
: $this->get_option('live_api_secret');
$this->webhook_secret = $this->get_option('webhook_secret');
// Hooks
add_action('woocommerce_update_options_payment_gateways_' . $this->id, [$this, 'process_admin_options']);
add_action('woocommerce_api_' . $this->id, [$this, 'webhook_handler']);
// Scripts
add_action('wp_enqueue_scripts', [$this, 'payment_scripts']);
// Additional hooks for advanced features
if ($this->supports('subscriptions')) {
$this->init_subscription_hooks();
}
if ($this->supports('tokenization')) {
$this->init_tokenization_hooks();
}
}
/**
* Check if gateway is available
*/
public function is_available() {
if ('yes' !== $this->enabled) {
return false;
}
// Check if API credentials are set
if (empty($this->api_key) || empty($this->api_secret)) {
return false;
}
// Currency check
if (!$this->is_currency_supported()) {
return false;
}
// Country check
if (!$this->is_country_supported()) {
return false;
}
// Custom availability checks
return apply_filters('woocommerce_gateway_' . $this->id . '_is_available', true, $this);
}
/**
* Check if currency is supported
*/
protected function is_currency_supported() {
$supported_currencies = $this->get_supported_currencies();
$current_currency = get_woocommerce_currency();
return in_array($current_currency, $supported_currencies, true);
}
/**
* Make API request
*/
protected function api_request($endpoint, $data = [], $method = 'POST') {
$url = $this->api_endpoint . $endpoint;
$args = [
'method' => $method,
'timeout' => 45,
'redirection' => 5,
'httpversion' => '1.1',
'blocking' => true,
'headers' => [
'Authorization' => 'Bearer ' . $this->api_key,
'Content-Type' => 'application/json',
'X-API-Version' => '2024-01',
'X-Client-Library' => 'WooCommerce/' . WC()->version,
],
];
if (!empty($data)) {
$args['body'] = json_encode($data);
}
// Log request in test mode
if ($this->test_mode) {
$this->log('API Request to ' . $endpoint . ': ' . print_r($data, true));
}
$response = wp_remote_request($url, $args);
if (is_wp_error($response)) {
$this->log('API Error: ' . $response->get_error_message());
throw new Exception($response->get_error_message());
}
$body = wp_remote_retrieve_body($response);
$code = wp_remote_retrieve_response_code($response);
// Log response in test mode
if ($this->test_mode) {
$this->log('API Response: ' . $body);
}
$data = json_decode($body, true);
if ($code >= 400) {
$error_message = isset($data['message']) ? $data['message'] : 'API request failed';
throw new Exception($error_message);
}
return $data;
}
/**
* Log gateway events
*/
protected function log($message) {
if ($this->test_mode) {
$logger = wc_get_logger();
$logger->debug($message, ['source' => $this->id]);
}
}
}
Implementing Core Gateway Methods
// Complete payment gateway implementation
class WC_Gateway_Custom_Provider extends WC_Payment_Gateway_Base {
/**
* Constructor
*/
public function __construct() {
$this->id = 'custom_provider';
$this->icon = plugins_url('assets/images/logo.png', __FILE__);
$this->has_fields = true;
$this->method_title = __('Custom Payment Provider', 'woocommerce');
$this->method_description = __('Accept payments through Custom Provider', 'woocommerce');
// Gateway supports
$this->supports = [
'products',
'refunds',
'subscriptions',
'subscription_cancellation',
'subscription_suspension',
'subscription_reactivation',
'subscription_amount_changes',
'subscription_date_changes',
'subscription_payment_method_change',
'subscription_payment_method_change_customer',
'subscription_payment_method_change_admin',
'multiple_subscriptions',
'tokenization',
'add_payment_method',
];
parent::__construct();
}
/**
* Initialize gateway settings
*/
public function init_form_fields() {
$this->form_fields = [
'enabled' => [
'title' => __('Enable/Disable', 'woocommerce'),
'type' => 'checkbox',
'label' => __('Enable Custom Payment Provider', 'woocommerce'),
'default' => 'yes',
],
'title' => [
'title' => __('Title', 'woocommerce'),
'type' => 'text',
'description' => __('This controls the title which the user sees during checkout.', 'woocommerce'),
'default' => __('Custom Payment', 'woocommerce'),
'desc_tip' => true,
],
'description' => [
'title' => __('Description', 'woocommerce'),
'type' => 'textarea',
'description' => __('Payment method description that the customer will see on your checkout.', 'woocommerce'),
'default' => __('Pay securely with Custom Provider.', 'woocommerce'),
],
'testmode' => [
'title' => __('Test mode', 'woocommerce'),
'type' => 'checkbox',
'label' => __('Enable Test Mode', 'woocommerce'),
'default' => 'yes',
'description' => __('Place the payment gateway in test mode using test API keys.', 'woocommerce'),
],
'test_api_key' => [
'title' => __('Test API Key', 'woocommerce'),
'type' => 'text',
'description' => __('Enter your test API key', 'woocommerce'),
'desc_tip' => true,
],
'test_api_secret' => [
'title' => __('Test API Secret', 'woocommerce'),
'type' => 'password',
'description' => __('Enter your test API secret', 'woocommerce'),
'desc_tip' => true,
],
'live_api_key' => [
'title' => __('Live API Key', 'woocommerce'),
'type' => 'text',
'description' => __('Enter your live API key', 'woocommerce'),
'desc_tip' => true,
],
'live_api_secret' => [
'title' => __('Live API Secret', 'woocommerce'),
'type' => 'password',
'description' => __('Enter your live API secret', 'woocommerce'),
'desc_tip' => true,
],
'webhook_secret' => [
'title' => __('Webhook Secret', 'woocommerce'),
'type' => 'password',
'description' => sprintf(
__('Enter the webhook secret. Webhook URL: %s', 'woocommerce'),
add_query_arg('wc-api', $this->id, home_url('/'))
),
],
'capture' => [
'title' => __('Capture', 'woocommerce'),
'type' => 'checkbox',
'label' => __('Capture payment immediately', 'woocommerce'),
'default' => 'yes',
'description' => __('Whether to capture payment immediately or authorize only.', 'woocommerce'),
],
'debug' => [
'title' => __('Debug Log', 'woocommerce'),
'type' => 'checkbox',
'label' => __('Enable logging', 'woocommerce'),
'default' => 'no',
'description' => sprintf(
__('Log events such as API requests. You can check the logs in %s', 'woocommerce'),
'<a href="' . admin_url('admin.php?page=wc-status&tab=logs') . '">WooCommerce > Status > Logs</a>'
),
],
];
}
/**
* Payment form on checkout page
*/
public function payment_fields() {
$description = $this->get_description();
if ($description) {
echo wpautop(wptexturize($description));
}
if ($this->supports('tokenization') && is_checkout()) {
$this->tokenization_script();
$this->saved_payment_methods();
}
?>
<fieldset id="wc-<?php echo esc_attr($this->id); ?>-form" class="wc-payment-form">
<?php do_action('woocommerce_credit_card_form_start', $this->id); ?>
<div class="form-row form-row-wide">
<label for="<?php echo esc_attr($this->id); ?>-card-number">
<?php esc_html_e('Card Number', 'woocommerce'); ?> <span class="required">*</span>
</label>
<input
id="<?php echo esc_attr($this->id); ?>-card-number"
class="input-text wc-credit-card-form-card-number"
type="text"
maxlength="20"
autocomplete="off"
placeholder="•••• •••• •••• ••••"
name="<?php echo esc_attr($this->id); ?>-card-number"
/>
</div>
<div class="form-row form-row-first">
<label for="<?php echo esc_attr($this->id); ?>-card-expiry">
<?php esc_html_e('Expiry (MM/YY)', 'woocommerce'); ?> <span class="required">*</span>
</label>
<input
id="<?php echo esc_attr($this->id); ?>-card-expiry"
class="input-text wc-credit-card-form-card-expiry"
type="text"
autocomplete="off"
placeholder="<?php esc_attr_e('MM / YY', 'woocommerce'); ?>"
name="<?php echo esc_attr($this->id); ?>-card-expiry"
/>
</div>
<div class="form-row form-row-last">
<label for="<?php echo esc_attr($this->id); ?>-card-cvc">
<?php esc_html_e('Card Code', 'woocommerce'); ?> <span class="required">*</span>
</label>
<input
id="<?php echo esc_attr($this->id); ?>-card-cvc"
class="input-text wc-credit-card-form-card-cvc"
type="text"
autocomplete="off"
placeholder="<?php esc_attr_e('CVC', 'woocommerce'); ?>"
name="<?php echo esc_attr($this->id); ?>-card-cvc"
/>
</div>
<?php if ($this->supports('tokenization') && is_user_logged_in()) : ?>
<div class="form-row form-row-wide">
<p class="form-row woocommerce-SavedPaymentMethods-saveNew">
<input
id="wc-<?php echo esc_attr($this->id); ?>-new-payment-method"
name="wc-<?php echo esc_attr($this->id); ?>-new-payment-method"
type="checkbox"
value="true"
/>
<label for="wc-<?php echo esc_attr($this->id); ?>-new-payment-method">
<?php esc_html_e('Save payment method', 'woocommerce'); ?>
</label>
</p>
</div>
<?php endif; ?>
<?php do_action('woocommerce_credit_card_form_end', $this->id); ?>
<div class="clear"></div>
</fieldset>
<?php
}
/**
* Process payment
*/
public function process_payment($order_id) {
$order = wc_get_order($order_id);
try {
// Validate payment fields
$this->validate_fields();
// Prepare payment data
$payment_data = $this->prepare_payment_data($order);
// Create payment intent
$intent = $this->api_request('payment_intents', $payment_data);
if (isset($intent['requires_action']) && $intent['requires_action']) {
// 3D Secure or additional authentication required
return [
'result' => 'success',
'redirect' => $intent['redirect_url'],
];
}
if ($intent['status'] === 'succeeded') {
// Payment successful
$order->payment_complete($intent['id']);
// Store payment metadata
$order->update_meta_data('_payment_intent_id', $intent['id']);
$order->update_meta_data('_payment_method_id', $intent['payment_method']);
// Save token if requested
if ($this->supports('tokenization') && isset($_POST['wc-' . $this->id . '-new-payment-method'])) {
$this->save_payment_token($order, $intent['payment_method']);
}
$order->save();
// Empty cart
WC()->cart->empty_cart();
// Return success
return [
'result' => 'success',
'redirect' => $this->get_return_url($order),
];
}
throw new Exception(__('Payment failed. Please try again.', 'woocommerce'));
} catch (Exception $e) {
wc_add_notice($e->getMessage(), 'error');
$this->log('Payment error: ' . $e->getMessage());
return [
'result' => 'fail',
'redirect' => '',
];
}
}
/**
* Validate payment fields
*/
public function validate_fields() {
$card_number = isset($_POST[$this->id . '-card-number'])
? wc_clean($_POST[$this->id . '-card-number'])
: '';
$card_expiry = isset($_POST[$this->id . '-card-expiry'])
? wc_clean($_POST[$this->id . '-card-expiry'])
: '';
$card_cvc = isset($_POST[$this->id . '-card-cvc'])
? wc_clean($_POST[$this->id . '-card-cvc'])
: '';
// Remove spaces and hyphens
$card_number = str_replace([' ', '-'], '', $card_number);
// Validate card number
if (empty($card_number) || !$this->is_valid_card_number($card_number)) {
wc_add_notice(__('Please enter a valid card number.', 'woocommerce'), 'error');
return false;
}
// Validate expiry
if (empty($card_expiry) || !$this->is_valid_expiry($card_expiry)) {
wc_add_notice(__('Please enter a valid expiry date.', 'woocommerce'), 'error');
return false;
}
// Validate CVC
if (empty($card_cvc) || !$this->is_valid_cvc($card_cvc)) {
wc_add_notice(__('Please enter a valid card security code.', 'woocommerce'), 'error');
return false;
}
return true;
}
/**
* Webhook handler
*/
public function webhook_handler() {
$payload = file_get_contents('php://input');
$sig_header = $_SERVER['HTTP_SIGNATURE'] ?? '';
try {
// Verify webhook signature
if (!$this->verify_webhook_signature($payload, $sig_header)) {
throw new Exception('Invalid signature');
}
$event = json_decode($payload, true);
switch ($event['type']) {
case 'payment_intent.succeeded':
$this->handle_payment_success($event['data']);
break;
case 'payment_intent.failed':
$this->handle_payment_failure($event['data']);
break;
case 'charge.refunded':
$this->handle_refund_webhook($event['data']);
break;
case 'charge.dispute.created':
$this->handle_dispute($event['data']);
break;
default:
$this->log('Unhandled webhook event: ' . $event['type']);
}
status_header(200);
exit;
} catch (Exception $e) {
$this->log('Webhook error: ' . $e->getMessage());
status_header(400);
exit($e->getMessage());
}
}
}
Building Your First Gateway
Step-by-Step Implementation
// Minimal working payment gateway
class WC_Gateway_Simple extends WC_Payment_Gateway {
public function __construct() {
$this->id = 'simple_gateway';
$this->icon = '';
$this->has_fields = false;
$this->method_title = 'Simple Gateway';
$this->method_description = 'A simple payment gateway example';
// Load settings
$this->init_form_fields();
$this->init_settings();
$this->title = $this->get_option('title');
$this->description = $this->get_option('description');
$this->enabled = $this->get_option('enabled');
// Save settings
add_action('woocommerce_update_options_payment_gateways_' . $this->id, [$this, 'process_admin_options']);
}
public function init_form_fields() {
$this->form_fields = [
'enabled' => [
'title' => 'Enable/Disable',
'type' => 'checkbox',
'label' => 'Enable Simple Gateway',
'default' => 'yes'
],
'title' => [
'title' => 'Title',
'type' => 'text',
'description' => 'This controls the title displayed during checkout.',
'default' => 'Simple Payment',
'desc_tip' => true,
],
'description' => [
'title' => 'Description',
'type' => 'textarea',
'description' => 'This controls the description displayed during checkout.',
'default' => 'Pay with our simple gateway.',
],
];
}
public function process_payment($order_id) {
$order = wc_get_order($order_id);
// Mark as on-hold (we're awaiting payment)
$order->update_status('on-hold', __('Awaiting payment confirmation', 'woocommerce'));
// Reduce stock
wc_reduce_stock_levels($order_id);
// Empty cart
WC()->cart->empty_cart();
// Return success
return [
'result' => 'success',
'redirect' => $this->get_return_url($order)
];
}
}
// Register the gateway
add_filter('woocommerce_payment_gateways', function($gateways) {
$gateways[] = 'WC_Gateway_Simple';
return $gateways;
});
Advanced Payment Features
Tokenization Implementation
// Token-based payment implementation
class WC_Gateway_Tokenized extends WC_Payment_Gateway_CC {
public function __construct() {
$this->supports = [
'products',
'refunds',
'tokenization',
'add_payment_method',
];
parent::__construct();
}
/**
* Add payment method via account page
*/
public function add_payment_method() {
try {
// Create payment method on provider
$payment_method = $this->create_payment_method();
// Create and save token
$token = new WC_Payment_Token_CC();
$token->set_token($payment_method['id']);
$token->set_gateway_id($this->id);
$token->set_card_type($payment_method['card']['brand']);
$token->set_last4($payment_method['card']['last4']);
$token->set_expiry_month($payment_method['card']['exp_month']);
$token->set_expiry_year($payment_method['card']['exp_year']);
$token->set_user_id(get_current_user_id());
$token->save();
return [
'result' => 'success',
'redirect' => wc_get_endpoint_url('payment-methods'),
];
} catch (Exception $e) {
wc_add_notice($e->getMessage(), 'error');
return [
'result' => 'failure',
];
}
}
/**
* Process payment with saved token
*/
protected function process_token_payment($token, $order) {
$payment_data = [
'amount' => $order->get_total() * 100, // Convert to cents
'currency' => $order->get_currency(),
'payment_method' => $token->get_token(),
'customer' => $this->get_customer_id($order),
'metadata' => [
'order_id' => $order->get_id(),
'order_key' => $order->get_order_key(),
],
];
$result = $this->api_request('charges', $payment_data);
if ($result['status'] === 'succeeded') {
$order->payment_complete($result['id']);
return true;
}
return false;
}
}
Subscription Support
// Subscription-compatible gateway
class WC_Gateway_Subscription_Ready extends WC_Payment_Gateway_Base {
public function __construct() {
$this->supports = array_merge($this->supports, [
'subscriptions',
'subscription_cancellation',
'subscription_suspension',
'subscription_reactivation',
'subscription_amount_changes',
'subscription_date_changes',
'subscription_payment_method_change',
'multiple_subscriptions',
]);
parent::__construct();
// Subscription hooks
add_action('woocommerce_scheduled_subscription_payment_' . $this->id, [$this, 'scheduled_subscription_payment'], 10, 2);
add_action('woocommerce_subscription_failing_payment_method_updated_' . $this->id, [$this, 'update_failing_payment_method'], 10, 2);
add_action('woocommerce_subscription_cancelled_' . $this->id, [$this, 'cancel_subscription']);
}
/**
* Process scheduled subscription payment
*/
public function scheduled_subscription_payment($renewal_total, $renewal_order) {
try {
$subscription = wcs_get_subscriptions_for_renewal_order($renewal_order);
$subscription = reset($subscription);
// Get saved payment method
$payment_token_id = $subscription->get_meta('_payment_token');
$token = WC_Payment_Tokens::get($payment_token_id);
if (!$token || $token->get_user_id() !== $subscription->get_user_id()) {
throw new Exception('Invalid payment token');
}
// Process payment
$payment_data = [
'amount' => $renewal_total * 100,
'currency' => $renewal_order->get_currency(),
'payment_method' => $token->get_token(),
'customer' => $this->get_customer_id($renewal_order),
'description' => sprintf(
'Subscription renewal for %s',
$subscription->get_id()
),
'metadata' => [
'order_id' => $renewal_order->get_id(),
'subscription_id' => $subscription->get_id(),
'renewal' => true,
],
];
$result = $this->api_request('charges', $payment_data);
if ($result['status'] === 'succeeded') {
$renewal_order->payment_complete($result['id']);
$subscription->add_order_note(
sprintf('Renewal payment processed successfully. Transaction ID: %s', $result['id'])
);
} else {
throw new Exception('Payment failed');
}
} catch (Exception $e) {
$renewal_order->update_status('failed', $e->getMessage());
$subscription->add_order_note(
sprintf('Renewal payment failed: %s', $e->getMessage())
);
}
}
}
Security and Compliance
PCI Compliance Implementation
// PCI-compliant tokenization
class WC_Gateway_PCI_Compliant extends WC_Payment_Gateway {
/**
* Enqueue secure payment scripts
*/
public function payment_scripts() {
if (!is_checkout() || !$this->is_available()) {
return;
}
// Provider's secure JS library
wp_enqueue_script(
$this->id . '-js',
'https://js.payment-provider.com/v3/',
[],
null,
true
);
// Our integration script
wp_enqueue_script(
$this->id . '-integration',
plugins_url('assets/js/checkout.js', __FILE__),
['jquery', $this->id . '-js'],
'1.0.0',
true
);
// Localize script with gateway params
wp_localize_script($this->id . '-integration', 'gateway_params', [
'public_key' => $this->get_public_key(),
'is_test_mode' => $this->test_mode,
'ajax_url' => WC_AJAX::get_endpoint('process_payment'),
'nonce' => wp_create_nonce('payment-nonce'),
'i18n' => [
'card_number_invalid' => __('Card number is invalid', 'woocommerce'),
'card_expiry_invalid' => __('Card expiry is invalid', 'woocommerce'),
'card_cvc_invalid' => __('Card CVC is invalid', 'woocommerce'),
'processing' => __('Processing...', 'woocommerce'),
],
]);
}
/**
* Tokenize card without touching sensitive data
*/
public function tokenize_payment_method() {
check_ajax_referer('payment-nonce', 'nonce');
try {
$token_data = json_decode(stripslashes($_POST['token_data']), true);
// Validate token with provider
$validation = $this->api_request('tokens/validate', [
'token' => $token_data['id'],
'fingerprint' => $this->get_device_fingerprint(),
]);
if (!$validation['valid']) {
throw new Exception('Invalid token');
}
// Return token for form submission
wp_send_json_success([
'token' => $token_data['id'],
'card' => [
'brand' => $validation['card']['brand'],
'last4' => $validation['card']['last4'],
],
]);
} catch (Exception $e) {
wp_send_json_error($e->getMessage());
}
}
}
Testing and Debugging
Comprehensive Testing Framework
// Payment gateway testing utilities
class WC_Gateway_Test_Helper {
private $gateway;
private $test_cards = [
'success' => '4242424242424242',
'declined' => '4000000000000002',
'insufficient_funds' => '4000000000000341',
'expired' => '4000000000000069',
'incorrect_cvc' => '4000000000000127',
'processing_error' => '4000000000000119',
'3d_secure' => '4000000000003220',
];
public function __construct($gateway) {
$this->gateway = $gateway;
}
/**
* Run comprehensive gateway tests
*/
public function run_tests() {
$results = [];
// Test API connectivity
$results['api_connection'] = $this->test_api_connection();
// Test payment processing
foreach ($this->test_cards as $scenario => $card_number) {
$results['payments'][$scenario] = $this->test_payment($card_number);
}
// Test refunds
$results['refunds'] = $this->test_refunds();
// Test webhooks
$results['webhooks'] = $this->test_webhooks();
// Test tokenization
if ($this->gateway->supports('tokenization')) {
$results['tokenization'] = $this->test_tokenization();
}
return $results;
}
/**
* Test payment processing
*/
private function test_payment($card_number) {
// Create test order
$order = wc_create_order([
'status' => 'pending',
'customer_id' => 1,
]);
$order->add_product(wc_get_product(1), 1);
$order->calculate_totals();
// Simulate payment data
$_POST[$this->gateway->id . '-card-number'] = $card_number;
$_POST[$this->gateway->id . '-card-expiry'] = '12/25';
$_POST[$this->gateway->id . '-card-cvc'] = '123';
// Process payment
$result = $this->gateway->process_payment($order->get_id());
// Clean up
wp_delete_post($order->get_id(), true);
return [
'success' => $result['result'] === 'success',
'message' => $result['message'] ?? 'Payment processed',
];
}
}
// Usage in development
if (defined('WP_DEBUG') && WP_DEBUG) {
add_action('admin_init', function() {
if (isset($_GET['test_gateway'])) {
$gateway = new WC_Gateway_Custom_Provider();
$tester = new WC_Gateway_Test_Helper($gateway);
$results = $tester->run_tests();
echo '<pre>';
print_r($results);
echo '</pre>';
exit;
}
});
}
Integration Best Practices
Error Handling and Recovery
// Robust error handling
class WC_Gateway_Error_Handler {
private $gateway;
private $max_retries = 3;
/**
* Process payment with retry logic
*/
public function process_payment_with_retry($order, $payment_data) {
$attempts = 0;
$last_error = null;
while ($attempts < $this->max_retries) {
try {
$attempts++;
// Add retry metadata
$payment_data['metadata']['attempt'] = $attempts;
// Process payment
$result = $this->gateway->api_request('charges', $payment_data);
if ($result['status'] === 'succeeded') {
return $result;
}
// Handle soft declines
if ($this->is_retryable_error($result)) {
$last_error = $result['error']['message'];
sleep(pow(2, $attempts)); // Exponential backoff
continue;
}
// Non-retryable error
throw new Exception($result['error']['message']);
} catch (Exception $e) {
$last_error = $e->getMessage();
// Check if network error (retryable)
if ($this->is_network_error($e) && $attempts < $this->max_retries) {
sleep(pow(2, $attempts));
continue;
}
// Log error
$this->log_payment_error($order, $e, $attempts);
throw $e;
}
}
// Max retries reached
throw new Exception(
sprintf('Payment failed after %d attempts: %s', $attempts, $last_error)
);
}
/**
* Webhook processing with idempotency
*/
public function process_webhook_idempotent($event) {
$event_id = $event['id'];
$processed_key = 'webhook_processed_' . $event_id;
// Check if already processed
if (get_transient($processed_key)) {
return ['status' => 'already_processed'];
}
// Lock to prevent concurrent processing
$lock_key = 'webhook_lock_' . $event_id;
if (!$this->acquire_lock($lock_key)) {
return ['status' => 'processing'];
}
try {
// Process webhook
$result = $this->gateway->handle_webhook_event($event);
// Mark as processed
set_transient($processed_key, true, DAY_IN_SECONDS * 7);
return $result;
} finally {
$this->release_lock($lock_key);
}
}
}
Building custom payment gateways requires careful attention to security, user experience, and reliability. By following these patterns and best practices, you can create professional payment integrations that handle millions in transactions while maintaining PCI compliance and providing excellent user experience.