# Payment (PSP) Connectors

> How to add a payment method to CBX — an integration with a payment service provider — by dropping a connector folder into the customization layer. A connecto…

Source: CBX documentation, version 3.x (released). Canonical page: https://docs.configbox.at/docs/customization/payment_connectors. Last updated 2026-08-01.

---
How to add a **payment method** to CBX — an integration with a payment service provider (PSP) — by
dropping a connector **folder** into the customization layer. A connector is a small, convention-based set
of files (no class hierarchy to learn): metadata functions, an admin settings form, a checkout "bridge"
that hands off to the PSP, a result page, and (if the PSP calls back) an IPN handler. CBX ships ~20
built-in connectors (PayPal, bank transfer, cash on delivery, and more) you can copy from.

This is a **customization-first shadow** mechanism: a connector folder in `data/customization/psp_connectors/`
is used instead of a built-in one of the same name, and new names simply add new methods.

Read `com_configbox_customization_overview.md` first. All paths are relative to the component root
`docroot/components/com_configbox/`. Source references are point-in-time (component **3.4.1**) — verify
against the code.

---

## 1. What a connector is

A connector is a **folder** named after the payment method (lowercase, `snake_case`), e.g.
`paypal_wpstandard`, `banktransfer`. The folder name **is** the connector name and is used to build every
file path, function name and class name (so keep it stable and filesystem-safe — it's run through
`sanitizeFileName()`, `helpers/psp.php:31`).

Resolution is **customization-first** (`ConfigboxPspHelper::getPspConnectorFolder()`, `:29-41`):

```php
if (is_dir(self::getCustomDir().'/'.$subFolder)) { return customDir/<name>; }   // customization wins
else                                             { return defaultDir/<name>; }   // else built-in
```

`getConnectorNames()` (`:16-22`) merges both dirs, so your custom connector appears in the payment-method
admin alongside the built-ins; a custom folder with a built-in's name **replaces** that built-in.

---

## 2. The folder contract (files, functions, classes)

Everything is convention. For a connector named `<name>`:

| File | Required? | What it provides |
|---|---|---|
| `administration.php` | **yes** | Plain functions describing the connector (title, setting keys, IPN support). See §3. |
| `settings.php` | if it has settings | The admin **settings form** template (the inputs for the keys in `_get_setting_keys()`). See §4. |
| `bridge.php` | **yes** | The **checkout hand-off** template — emits the form/redirect to the PSP. Rendered as `ConfigboxViewCheckoutpspbridge`. See §5. |
| `result.php` | usually | The **return/confirmation page** template, rendered as `ConfigboxViewPaymentresult`. See §6. |
| `ipn.php` | only if IPN | The **instant payment notification** handler class. See §7. |
| `language/` | optional | Connector-specific language files. |

> There is **no base class**. A connector is files + named functions + (for IPN) one class — by
> convention, resolved by `ConfigboxPspHelper`. This is deliberately lightweight; follow a built-in
> connector as your template.

---

## 3. `administration.php` — the metadata functions

Plain functions, **prefixed with the connector name** (this is how the helper finds them). From the
built-in `paypal_wpstandard/administration.php`:

```php
<?php
function paypal_wpstandard_get_title() {                       // shown in the admin payment-method list
    return 'PayPal Payments Standard';
}
function paypal_wpstandard_get_setting_keys() {                // POST keys stored as this method's settings
    return array('paypalid', 'testmode');
}
function paypal_wpstandard_has_instant_payment_notification() {// does the PSP call back? (IPN)
    return true;
}
function paypal_wpstandard_get_product_url() {                 // optional: a "learn more" link
    return 'https://www.paypal.com/…';
}
```

| Function | Read by | Purpose |
|---|---|---|
| `<name>_get_title()` | `getPspConnectorTitle()` (`:79-95`) | Display name (falls back to the folder name). |
| `<name>_get_setting_keys()` | `getPspConnectorSettingKeys()` (`:50-72`) | The keys taken from POST when the payment method is saved; these become the connector's **settings**, available in `settings.php`, the order record's payment object, and the IPN class. |
| `<name>_has_instant_payment_notification()` | `pspSupportsIpn()` (`:102-119`) | `true` ⇒ CBX expects an `ipn.php` (§7). |

> The smallest possible `administration.php` is just `<name>_get_title()` +
> `<name>_has_instant_payment_notification()` returning `false` (e.g. the built-in `banktransfer`). A method
> with no PSP hand-off needs no settings and no IPN.

---

## 4. `settings.php` — the admin settings form

A plain PHP template rendering the inputs for the keys in `_get_setting_keys()`. `$this` is the
payment-method admin view; read current values with `$this->settings->get('<key>')` and **name each input
after its setting key** so it's picked up from POST on save. From `paypal_wpstandard/settings.php`:

```php
<?php defined('CB_VALID_ENTRY') or die(); ?>

<div class="kenedo-property property-type-string">
    <div class="property-label"><?php echo KText::_('PayPal email address'); ?></div>
    <div class="property-body">
        <input class="form-control" type="text" name="paypalid"
               value="<?php echo hsc($this->settings->get('paypalid')); ?>" />
    </div>
</div>
```

The input `name="paypalid"` matches a key returned by `paypal_wpstandard_get_setting_keys()`, so saving the
payment method stores it. Escape values with `hsc()`.

---

## 5. `bridge.php` — the checkout hand-off

After checkout, CBX renders the connector's `bridge.php` as the view
`ConfigboxViewCheckoutpspbridge`. This is where you POST/redirect to the PSP. For a **redirect** PSP you
typically emit a form (auto-submitted by the checkout JS) with the PSP's fields and your settings; for an
**offline** method you just link to the result page. From `banktransfer/bridge.php` (offline — no PSP):

```php
<?php
defined('CB_VALID_ENTRY') or die();
/** @var $this ConfigboxViewCheckoutpspbridge */
?>
<div id="checkoutform">
    <a class="trigger-redirect-to-psp"
       href="<?php echo KLink::getRoute('index.php?option=com_configbox&view=paymentresult&connector_name=banktransfer'); ?>"></a>
</div>
```

The `a.trigger-redirect-to-psp` element is the convention the checkout JS follows to advance. A real PSP
bridge builds a `<form>` whose `action` is the PSP endpoint and whose hidden inputs carry the amount, order
id, return URLs, and credentials from `$this->settings`. Copy a redirecting built-in (e.g.
`paypal_wpstandard/bridge.php`) as your starting point.

---

## 6. `result.php` — the return / confirmation page

Rendered as `ConfigboxViewPaymentresult` (the `view=paymentresult` URL the bridge points at). `$this`
exposes the order and shop context — `$this->orderRecord` (incl. `payableAmount`, `id`), `$this->shopdata`,
and link vars (`$this->linkToOrder`, `$this->linkToCustomerProfile`, `$this->linkToDefaultProductListing`).
From `banktransfer/result.php` (abridged):

```php
<?php
defined('CB_VALID_ENTRY') or die();
/** @var ConfigboxViewPaymentresult $this */
?>
<div id="com_configbox" class="cb-content">
  <div id="view-paymentresult">
    <h2><?php echo KText::_('Payment via bank transaction'); ?></h2>
    <p><?php echo KText::sprintf('Please transfer %s to the following account:', cbprice($this->orderRecord->payableAmount)); ?></p>
    <!-- … account details from $this->shopdata, escaped with hsc() … -->
    <ul class="continue-links">
      <li><a href="<?php echo $this->linkToOrder; ?>"><?php echo KText::_('See order details'); ?></a></li>
    </ul>
  </div>
</div>
```

Use `cbprice()` for money and `hsc()` for everything dynamic.

---

## 7. `ipn.php` — instant payment notification (when the PSP calls back)

If `_has_instant_payment_notification()` returns `true`, CBX routes the PSP's server-to-server
callback to your IPN class. `getIpnObject()` (`:126-155`) loads `ipn.php` and instantiates the class:

```php
$className = 'Ipn' . ucwords(strtolower($connectorName));   // :146
```

> ⚠️ **The IPN class name has a quirk.** `ucwords()` capitalizes after **spaces**, not underscores, and the
> name is first lowercased. So for connector `paypal_wpstandard` the expected class is
> **`Ipnpaypal_wpstandard`** (one leading capital, the rest lowercase, underscore preserved) — *not*
> `IpnPaypalWpstandard`. Name your IPN class exactly `'Ipn'.ucwords(strtolower('<name>'))`; copy a
> built-in's `ipn.php` and rename to be safe. A mismatch throws "IPN class not found" (`:148-151`).

The IPN class verifies the PSP's callback (authenticity + amount), then sets the order's status — usually
by resolving a status code and updating the order. Settings from `_get_setting_keys()` are available to the
IPN object (e.g. to pick test vs. live credentials). Use a built-in IPN handler (e.g.
`paypal_wpstandard/ipn.php`) as the structural reference; the verification details are PSP-specific.

> **Security note:** the IPN endpoint is **public and unauthenticated by CBX** — anyone can POST to
> it. Your handler is the only thing standing between a forged callback and a "paid" order. Always verify
> the notification with the PSP (signature/round-trip), check the amount and currency against the order,
> and be idempotent (the PSP may retry). SQL here is hand-built mysqli — quote/validate everything from the
> request.

---

## 8. Worked example — a minimal "Invoice on account" method

An offline method (no PSP, no IPN): the customer is told they'll be invoiced. Folder
`data/customization/psp_connectors/invoiceaccount/`.

```php
// administration.php
<?php
function invoiceaccount_get_title() {
    return KText::_('Invoice on account');
}
function invoiceaccount_has_instant_payment_notification() {
    return false;     // offline — no callback, no ipn.php needed
}
// (no _get_setting_keys → no settings.php needed)
```

```php
// bridge.php   (rendered as ConfigboxViewCheckoutpspbridge)
<?php
defined('CB_VALID_ENTRY') or die();
/** @var $this ConfigboxViewCheckoutpspbridge */
?>
<div id="checkoutform">
    <a class="trigger-redirect-to-psp"
       href="<?php echo KLink::getRoute('index.php?option=com_configbox&view=paymentresult&connector_name=invoiceaccount'); ?>"></a>
</div>
```

```php
// result.php   (rendered as ConfigboxViewPaymentresult)
<?php
defined('CB_VALID_ENTRY') or die();
/** @var ConfigboxViewPaymentresult $this */
?>
<div id="com_configbox" class="cb-content"><div id="view-paymentresult">
    <h2><?php echo KText::_('Thank you for your order'); ?></h2>
    <p><?php echo KText::sprintf('We will invoice the amount of %s. Order number %s.',
            cbprice($this->orderRecord->payableAmount), $this->orderRecord->id); ?></p>
    <ul class="continue-links">
        <li><a href="<?php echo $this->linkToOrder; ?>"><?php echo KText::_('See order details'); ?></a></li>
    </ul>
</div></div>
```

Three files, no class. The method now appears in the admin payment-method list (title from
`administration.php`) and works end-to-end at checkout. Enable/assign it like any payment method
(`functional/com_configbox_commerce_setup.md`).

---

## 9. Deployment checklist

```
data/customization/psp_connectors/<name>/
  administration.php   ← <name>_get_title(), _get_setting_keys(), _has_instant_payment_notification()
  settings.php         ← admin settings form (only if there are setting keys)
  bridge.php           ← checkout hand-off (ConfigboxViewCheckoutpspbridge)
  result.php           ← return/confirmation page (ConfigboxViewPaymentresult)
  ipn.php              ← class Ipn<ucwords(strtolower(name))> (only if IPN supported)
  language/            ← optional connector language files
```

1. **Pick a stable, lowercase folder name** = the connector name (drives all paths/functions/classes).
2. **Write `administration.php`** with the three core functions (title, setting keys, IPN flag).
3. **If it has settings**, add `settings.php` with inputs named after the keys; read with
   `$this->settings->get()`.
4. **Write `bridge.php`** (PSP redirect form, or a link to `view=paymentresult` for offline).
5. **Write `result.php`** for the customer-facing confirmation.
6. **If the PSP calls back**, set `_has_instant_payment_notification()` to `true`, add `ipn.php` with class
   **`Ipn<ucwords(strtolower(name))>`** (§7), and **verify every callback** (auth + amount + idempotency).
7. **Configure the payment method** in the admin (assign to groups/zones) — see the commerce-setup doc.
8. **Verify manually** end-to-end — checkout → bridge → PSP → IPN → status change → result page — in the
   PSP's sandbox. CBX has **zero automated tests**.

---

## 10. Conventions & gotchas

- **Folder name = connector name = path/function/class prefix.** Keep it lowercase, `snake_case`, stable.
- **Customization-first.** A custom folder shadows a built-in of the same name install-wide; new names add
  new methods.
- **Functions are name-prefixed** (`<name>_get_title`, …) — that's how the helper resolves them.
- **IPN class name quirk** — `'Ipn'.ucwords(strtolower($name))` capitalizes after spaces only, so
  `paypal_wpstandard` → `Ipnpaypal_wpstandard` (§7). Copy + rename from a built-in.
- **IPN is public + unauthenticated** — verify with the PSP, check amount/currency, be idempotent. This is
  the security-critical surface of any payment integration.
- **Templates, not classes** (except IPN). `bridge.php`/`result.php` are view templates with `$this` bound
  to the respective core view; `settings.php` to the admin view.
- **Escape & quote.** `hsc()` for output, `cbprice()` for money; hand-built mysqli — never trust request
  data in the IPN handler.
- **Copy a built-in** that matches your PSP's flow (redirect vs. offline vs. API) as your starting point —
  the ~20 in `psp_connectors/` cover most patterns.

---

## See also

- `psp_connectors/` — the ~20 built-in connectors (banktransfer, paypal_wpstandard, cashondelivery, …) to
  copy from.
- `helpers/psp.php` — `ConfigboxPspHelper`: resolution (`getPspConnectorFolder`, `:29`), metadata
  (`:50`,`:79`,`:102`), IPN loading (`getIpnObject`, `:126`).
- `functional/com_configbox_commerce_setup.md` — configuring/assigning payment methods in the admin.
- `com_configbox_customization_overview.md` — the extension-point map.
- `com_configbox_events_and_observers.md` — `onConfigboxGetPaymentOptions` (offer payment options
  programmatically) and `onConfigBoxSetStatus` (react to the status change an IPN triggers).
