CartThrob 9 documentation for ExpressionEngine commerce teams Visit CartThrob.com
CartThrob.com

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.php for a direct Omnipay charge + refund flow
  • Cartthrob_ct_offline_payments.php for a simple gateway without card processing
  • Cartthrob_stripe.php for 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:

  1. Lives in CartThrob’s payment gateway plugins folder
  2. Extends CartThrob\Plugins\Payment\PaymentPlugin
  3. Declares a $title and checkout field metadata
  4. Implements charge($creditCardNumber) and returns a TransactionState

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:

  1. The filename and classname must match.
  2. The classname must begin with Cartthrob_.
  3. The file must begin with Cartthrob_.
  4. 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 TransactionState with 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.';
  • $title is required.
  • $overview and $note are 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:

  1. Start checkout with charge() returning a processing/offsite state or redirect data
  2. Implement ExtloadInterface::extload() or use CartThrob’s offsite completion helpers
  3. Finish with checkoutCompleteOffsite() or related PaymentPlugin delegated 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:

  1. File is in system/user/addons/cartthrob/cartthrob/plugins/payment_gateways/
  2. Filename and classname match
  3. Classname starts with Cartthrob_
  4. Class extends CartThrob\Plugins\Payment\PaymentPlugin
  5. charge() exists and returns TransactionState
  6. $fields and $required_fields match what checkout needs
  7. Optional capabilities use the correct interface (RefundInterface, TokenInterface, etc.)
  8. Gateway is enabled and configured in CartThrob Payments settings
  9. 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