Payment Gateway Plugins
This guide explains how to build a custom CartThrob payment gateway plugin using the CartThrob 9.x plugin API. It assumes basic PHP knowledge: classes, methods, arrays, and returning values.
If you need ExpressionEngine add-on basics first, see the ExpressionEngine Docs and the Add-on Development Overview.
CartThrob ships with several working payment gateways. Start by copying one of those and changing it, rather than writing everything from scratch.
Good starter examples:
Cartthrob_dummy_gateway.phpfor a direct Omnipay charge + refund flowCartthrob_ct_offline_payments.phpfor a simple gateway without card processingCartthrob_stripe.phpfor a full production gateway with tokens, refunds, and voids
Payment gateways return CartThrob\Transactions\TransactionState objects, not raw arrays or Money objects.
For additional sample branches and historical examples, see the CartThrob Payment Gateway Samples Wiki. Use that repo as a supplement, not a replacement for this 9.x API guide.
Start Here
A payment gateway plugin is a PHP class that:
- Lives in CartThrob’s payment gateway plugins folder
- Extends
CartThrob\Plugins\Payment\PaymentPlugin - Declares a
$titleand checkout field metadata - Implements
charge($creditCardNumber)and returns aTransactionState
At a minimum, CartThrob needs that charge() method so checkout can process a payment.
If your gateway also supports refunds, saved cards, offsite callbacks, voids, or subscriptions, implement the matching optional interface and methods.
File Location and Naming
Store payment gateway plugins here:
system/user/addons/cartthrob/cartthrob/plugins/payment_gateways/
You can also add extra gateway paths through CartThrob’s payment gateway loader if you are building a separate add-on.
Rules:
- The filename and classname must match.
- The classname must begin with
Cartthrob_. - The file must begin with
Cartthrob_. - Do not start your own PHP session. CartThrob already manages session and cart state.
Example:
Filename: Cartthrob_my_gateway.php
Class: Cartthrob_my_gateway
After adding the file, enable and configure the gateway in CartThrob Payments settings.
Minimum Working Plugin
This is the smallest useful direct gateway: approve the charge and return an authorized transaction.
<?php
use CartThrob\Plugins\Payment\PaymentPlugin;
use CartThrob\Transactions\TransactionState;
if (!defined('CARTTHROB_PATH')) {
Cartthrob_core::core_error('No direct script access allowed');
}
class Cartthrob_my_gateway extends PaymentPlugin
{
public $title = 'My Gateway';
public $fields = [
'first_name',
'last_name',
'email_address',
'credit_card_number',
'expiration_month',
'expiration_year',
];
public $required_fields = [
'first_name',
'last_name',
'email_address',
'credit_card_number',
'expiration_month',
'expiration_year',
];
public function charge($creditCardNumber)
{
// Send $creditCardNumber and $this->total() to your processor here
return $this->authorize('txn_' . time());
}
}
What this does:
- Declares which checkout fields the gateway needs
- Receives the sanitized card number in
charge() - Returns an authorized
TransactionStatewith a transaction ID
Plugin Information
These public properties describe the gateway in the control panel and checkout.
public $title = 'My Gateway';
public $overview = 'Optional longer overview text.';
public $note = 'Optional note shown with gateway settings.';
$titleis required.$overviewand$noteare optional.- You can use plain text, or a language key that CartThrob will look up.
Other common public properties:
public $payment_details_available = true;
public $form_extra = '<script>...</script>';
public array $nameless_fields = [];
public array $extra_fields = [];
Checkout Fields and Settings
$fields
Array of checkout/customer field short names your gateway wants available during checkout, such as:
public $fields = [
'first_name',
'last_name',
'email_address',
'credit_card_number',
'expiration_month',
'expiration_year',
];
CartThrob uses this list when building gateway field output for templates.
$required_fields
Subset of fields that must be present before checkout can continue.
$settings
Admin-configurable gateway settings, using the same pattern as tax/shipping/discount plugins:
public $settings = [
[
'name' => 'Mode',
'short_name' => 'mode',
'type' => 'select',
'default' => 'test',
'options' => [
'test' => 'Test',
'live' => 'Live',
],
],
[
'name' => 'API key',
'short_name' => 'api_key',
'type' => 'text',
],
];
Read settings in your gateway with:
$this->plugin_settings('mode');
$this->plugin_settings('api_key');
Language files
Built-in gateway labels often live in:
system/user/addons/cartthrob/language/english/cartthrob_lang.php
You can also add a plugin-specific language file next to the gateway if needed.
Required and Optional Methods
charge($creditCardNumber)
Required for direct gateways. CartThrob calls this through ee()->cartthrob_payments->charge().
Parameters:
$creditCardNumber: sanitized card number from checkout
Returns: TransactionState
public function charge($creditCardNumber)
{
try {
// Call your payment processor
return $this->authorize($transactionId);
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
}
Important:
- Return a
TransactionState, not a boolean or array - Use
$this->authorize($transactionId)for successful charges - Use
$this->fail($message)for failures - Use
$this->processing($transactionId)when the payment is not finished yet
initialize($params = [], $defaults = [])
Optional setup hook. Use it to configure Omnipay, read mode-specific API keys, or prepare gateway state.
form_extra()
Optional. Returns extra HTML or JavaScript needed by the gateway checkout UI. The base class provides a default tokenizer script for some gateways.
Optional Capability Interfaces
Implement only what your processor supports.
| Interface | Purpose | Key methods |
|---|---|---|
RefundInterface |
Refund from Order Manager | refund($transactionId, $amount, $creditCardNumber, $extra) |
TokenInterface |
Saved cards / vault charges | createToken($creditCardNumber), chargeToken($token, $customer_id) |
VoidInterface |
Void uncaptured payments | void($paymentIntent, $extra = []) |
ExtloadInterface |
Offsite return/callback handling | extload(array $data) |
RecurrentBillingInterface |
Subscriptions | createRecurrentBilling(...), updateRecurrentBilling(...) |
Example:
use CartThrob\Plugins\Payment\PaymentPlugin;
use CartThrob\Plugins\Payment\RefundInterface;
class Cartthrob_my_gateway extends PaymentPlugin implements RefundInterface
{
public function refund($transactionId, $amount, $creditCardNumber, $extra): TransactionState
{
// Call processor refund API
return $this->authorize($refundTransactionId);
}
}
If a method is not implemented, CartThrob will report that the active gateway does not support that action.
TransactionState
TransactionState is how CartThrob understands payment results.
Common success helpers on PaymentPlugin:
return $this->authorize($transactionId);
return $this->processing($transactionId);
Failure helper:
return $this->fail('Payment was declined.');
You can also build states directly:
$state = new TransactionState();
$state->setAuthorized()->setTransactionId($transactionId);
return $state;
Useful status setters:
setAuthorized()setProcessing()setDeclined()setFailed()setRefunded()setPending()setCanceled()setExpired()
CartThrob uses these flags to decide which checkout notifications, order statuses, and cart actions run next.
How CartThrob Calls Your Gateway
Understanding the call sites helps you implement the right methods.
Checkout charge
For a normal direct checkout, CartThrob eventually does:
$state = ee()->cartthrob_payments->charge($creditCardNumber);
That delegates to your gateway’s charge() method.
Checkout start
Cartthrob_payments::checkoutStart() validates checkout data, creates or updates the order, and then calls the gateway charge/token flow. Your gateway usually does not replace this method; it plugs into it through charge() or token methods.
Refunds
Order Manager refunds call:
ee()->cartthrob_payments->refund($transactionId, $amount, $creditCardNumber, $extra);
That only works if your gateway implements RefundInterface.
Gateway fields in templates
Templates use:
{exp:cartthrob:gateway_fields}
That reads the active gateway’s $fields, $required_fields, and optional custom template setting.
Offsite gateways
Some gateways redirect customers away from your site and complete payment through a callback. Those gateways usually:
- Start checkout with
charge()returning a processing/offsite state or redirect data - Implement
ExtloadInterface::extload()or use CartThrob’s offsite completion helpers - Finish with
checkoutCompleteOffsite()or relatedPaymentPlugindelegated methods
Use bundled Sage, PayPal, or Mollie gateways as references for offsite behavior.
Useful Gateway Helpers
PaymentPlugin delegates many Cartthrob_payments methods to your gateway instance.
Common helpers:
$this->total();
$this->order();
$this->orderId();
$this->gateway();
$this->plugin_settings('api_key');
$this->addError('field_name', 'Message');
Offsite and order helpers:
$this->getNotifyUrl($gateway, $method);
$this->completePaymentOffsite($url, $offsiteData);
$this->checkoutCompleteOffsite($state, $orderId);
$this->jumpForm($url, $fields);
HTTP helpers:
$this->curlTransaction($url, $data);
$this->curlPost($url, $params);
Use ee() for ExpressionEngine access:
ee()->input->post('expiration_month');
ee()->lang->line('some_key');
Do not use $this->EE =& get_instance(); in new gateways.
Omnipay Gateways
Many bundled gateways use Omnipay through CartThrob’s namespaced dependency:
use CartThrob\Dependency\Omnipay\Omnipay;
use CartThrob\Dependency\Omnipay\Dummy\Gateway as OmnipayGateway;
protected $omnipayGateway;
public function __construct()
{
$this->omnipayGateway = Omnipay::create('Dummy');
$this->omnipayGateway->initialize([]);
}
public function charge($creditCardNumber)
{
$response = $this->omnipayGateway->purchase([
'amount' => $this->total(),
'card' => [
'number' => $creditCardNumber,
'expiryMonth' => ee()->input->post('expiration_month'),
'expiryYear' => ee()->input->post('expiration_year'),
],
])->send();
if (!$response->isSuccessful()) {
return $this->fail($response->getMessage());
}
return $this->authorize($response->getTransactionReference());
}
Cartthrob_dummy_gateway.php is the best minimal Omnipay example. Cartthrob_stripe.php shows a production gateway with custom request/response handling.
Validation Rules
Many current gateways validate admin settings with a $rules array:
protected array $rules = [
'api_key' => 'required',
'mode' => 'required',
];
This helps catch missing credentials before checkout fails at runtime.
Examples
Example 1: Offline-style gateway
Same pattern as Cartthrob_ct_offline_payments.php.
public function charge($creditCardNumber)
{
$state = new TransactionState();
return $state
->setProcessing('Awaiting payment')
->setTransactionId('offline_' . time());
}
Example 2: Dummy Omnipay gateway
Use Cartthrob_dummy_gateway.php when you want a working direct gateway with refunds and no real processor.
Example 3: Tokenized gateway
Implement TokenInterface when the gateway stores a card token and charges it later:
use CartThrob\Plugins\Payment\TokenInterface;
use Cartthrob_token;
class Cartthrob_my_gateway extends PaymentPlugin implements TokenInterface
{
public function createToken($creditCardNumber): Cartthrob_token
{
return new Cartthrob_token(['token' => 'tok_' . uniqid()]);
}
public function chargeToken($token, $customer_id): TransactionState
{
// Charge saved token
return $this->authorize('txn_' . time());
}
}
Checklist
Before testing your gateway:
- File is in
system/user/addons/cartthrob/cartthrob/plugins/payment_gateways/ - Filename and classname match
- Classname starts with
Cartthrob_ - Class extends
CartThrob\Plugins\Payment\PaymentPlugin charge()exists and returnsTransactionState$fieldsand$required_fieldsmatch what checkout needs- Optional capabilities use the correct interface (
RefundInterface,TokenInterface, etc.) - Gateway is enabled and configured in CartThrob Payments settings
- Test success, decline/failure, and any refund/token/offsite paths you support
If the gateway does not appear in the control panel, check the classname prefix and file location first. If checkout errors immediately, verify that charge() returns TransactionState and that failed responses use fail() or setFailed().
ExpressionEngine References
Additional Resources
- CartThrob Payment Gateway Samples repo
- Payment Gateway Samples Wiki
- Hooks reference for
cartthrob_checkout_action_start,cartthrob_on_authorize,cartthrob_on_decline, and refund hooks