| name | perfex-payment-gateway |
| description | Use whenever the user is creating, modifying, or debugging a Perfex CRM payment gateway module — a class extending `App_gateway` in `modules/<module>/libraries/<Id>_gateway.php`, calling `setId`, `setName`, `setSettings`, implementing `process_payment($data)`, or registering via `register_payment_gateway`. Also trigger when the user says "create a payment gateway for Perfex", "my gateway webhook gets CSRF blocked", "process_payment not firing", "Stripe/PayPal/Mollie integration for Perfex", "payment gateway settings not saving", "encrypted setting", or "how do I handle the payment callback in Perfex". Covers the App_gateway lifecycle, settings encryption, webhook CSRF exclusion, and the Stripe API (Basil) changes in Perfex 3.3.0. |
| license | MIT |
| metadata | {"author":"yasserstudio","version":"1.4.0"} |
Perfex Payment Gateway Development
You are a Perfex CRM payment-gateway engineer. Your job is to build gateway modules that extend App_gateway correctly — with encrypted secrets, proper webhook CSRF exclusion, and defensive callback handling — so payments process reliably across Stripe API updates and concurrent invoice payments.
Perfex supports custom payment gateways as modules since v2.3.4. A gateway is a class in modules/<module>/libraries/<Id>_gateway.php that extends App_gateway and implements process_payment($data).
File structure
modules/my_gateway/
├── my_gateway.php # module entry (hooks, register_payment_gateway)
├── install.php # optional: module-owned tables (transaction log, webhook log)
├── libraries/
│ └── My_gateway_gateway.php # class My_gateway_gateway extends App_gateway
├── controllers/
│ └── My_gateway_webhook.php # webhook receiver (CSRF-excluded)
├── views/
│ └── payment_form.php # optional inline payment form
└── language/
└── english/
└── my_gateway_lang.php
The library filename must end with _gateway.php. Class name must match filename (capitalized first letter).
Gateway class skeleton
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class My_gateway_gateway extends App_gateway
{
public function __construct()
{
parent::__construct();
$this->setId('my_gateway');
$this->setName('My Gateway');
$this->setSettings([
[
'name' => 'api_key',
'encrypted' => true,
'label' => 'API Key',
'type' => 'input',
],
[
'name' => 'api_secret',
'encrypted' => true,
'label' => 'API Secret',
'type' => 'input',
],
[
'name' => 'test_mode',
'label' => 'Test Mode',
'type' => 'yes_no',
'default_value' => '1',
],
[
'name' => 'currencies',
'label' => 'settings_paymentmethod_currencies',
'default_value' => 'USD,EUR',
],
]);
}
public function process_payment($data)
{
$invoice = $data['invoice'];
$amount = $data['amount'];
$currency = $data['currency'];
$api_key = $this->decryptSetting('api_key');
redirect($checkout_url);
}
}
Registration (module entry file)
register_payment_gateway('my_gateway_gateway', 'my_gateway');
First param: class name (lowercase). Second param: module system name. After activation, the gateway appears in Setup → Settings → Payment Gateways.
Settings system
| Type | Renders as | Stored as |
|---|
input | Text input | Plain or encrypted string |
textarea | Multi-line input | Plain or encrypted string |
yes_no | Toggle switch | '1' or '0' |
| (no type) | Text input | Plain string |
Encrypted settings
Set 'encrypted' => true on any setting holding secrets (API keys, webhook signing keys). Perfex encrypts at rest using the application encryption key. Access via $this->decryptSetting('name') — never read directly from DB.
$secret = $this->decryptSetting('api_secret');
$secret = $this->getSetting('api_secret');
Reading non-encrypted settings
$mode = $this->getSetting('test_mode');
$currencies = explode(',', $this->getSetting('currencies'));
Webhook handling
External gateways POST payment confirmations to your callback URL. Two requirements:
1. CSRF exclusion
Perfex's global CSRF protection blocks external POSTs. Exclude your webhook route in application/config/config.php:
$config['csrf_exclude_uris'] = array_merge(
$config['csrf_exclude_uris'] ?? [],
['my_gateway_webhook/handle']
);
Or via hook in your module entry:
hooks()->add_filter('csrf_exclude_uris', function ($uris) {
$uris[] = 'my_gateway_webhook/handle';
return $uris;
});
2. Webhook controller
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class My_gateway_webhook extends CI_Controller
{
public function handle()
{
$payload = file_get_contents('php://input');
$sig = $this->input->get_request_header('X-Signature');
$secret = $this->my_gateway_gateway->decryptSetting('webhook_secret');
if (!$this->verify_signature($payload, $sig, $secret)) {
log_message('error', 'my_gateway: webhook signature mismatch');
$this->output->set_status_header(401);
return;
}
$event = json_decode($payload, true);
if ($event['type'] === 'payment.completed') {
$this->process_successful_payment($event);
}
$this->output->set_status_header(200);
}
private function process_successful_payment($event)
{
$invoice_id = $event['metadata']['invoice_id'];
$this->load->model('payments_model');
$payment_data = [
'invoiceid' => $invoice_id,
'amount' => $event['amount'] / 100,
'paymentmode' => 'my_gateway',
'transactionid' => $event['transaction_id'],
'date' => date('Y-m-d'),
];
$this->payments_model->add($payment_data);
}
private function verify_signature($payload, $sig, $secret)
{
$expected = hash_hmac('sha256', $payload, $secret);
return hash_equals($expected, $sig);
}
}
The $data array in process_payment
Perfex passes an array containing:
| Key | Type | Description |
|---|
invoice | object | Full invoice row from tblinvoices |
amount | float | Amount to charge (may be partial payment) |
currency | object | Currency info (name, symbol, decimal_separator, etc.) |
Access the invoice ID as $data['invoice']->id. The amount respects partial-payment settings — don't assume it equals the invoice total.
Stripe API (Basil) changes — Perfex 3.3.0+
Perfex 3.3.0 updated to Stripe API version "Basil". If your module wraps Stripe:
- Webhooks must be recreated after upgrading to 3.3.0 — event payload format changed
- Stripe now respects allowed payment methods from the Stripe Dashboard (no longer hardcoded in Perfex)
- The
after_invoice_added hook now fires before email sending (changed in 3.2.0) — if your gateway listens to this hook to auto-charge, the invoice email may not have been sent yet
Existing gateway reference
Study Perfex's built-in gateways for patterns:
application/libraries/gateways/ — Stripe, PayPal, 2Checkout, Mollie
application/controllers/gateways/ — webhook receivers for built-in gateways
Common pitfalls
- Filename must end with
_gateway.php — My_gateway.php alone won't be detected.
setId() must be alphanumeric only — no hyphens, no underscores. Use snake_case in class name but plain string for ID.
- Don't store card data — let the external gateway handle PCI compliance. Your module only stores transaction IDs.
process_payment is called on every "Pay Now" click — it must be idempotent or create a new checkout session each time. Don't create duplicate charges.
- Test with multiple currencies — the
currencies setting is a comma-separated string that Perfex checks before showing the gateway as available for an invoice.
- Webhook retries — most gateways retry failed webhooks. Your handler must be idempotent (check if payment already recorded before inserting).
Related skills
perfex-security — CSRF exclusion mechanics, webhook signature verification, app_generate_hash() for nonces.
perfex-module-dev — module lifecycle, register_payment_gateway() lives in the module entry file.
perfex-database — if you add a tbl<module>_transactions table for logging.
Upstream docs