Most merchants hit a wall when their payment gateway isn’t supported by an existing Magento extension. Whether you’re working with a regional processor, a B2B invoicing system, or a proprietary payment flow, the only real solution is to build a custom Magento 2 payment module yourself.
This guide covers exactly how to do that — from file structure and configuration to the JavaScript renderer and checkout template. By the end, you’ll have a working module and a clear understanding of where most developers go wrong.
Summary
- What a Magento 2 payment module is and why building a custom one matters
- The core file structure you need before writing a single line of code
- A step-by-step walkthrough to create a Magento 2 payment module from scratch
- How to configure the payment model, renderer, and checkout template
- Common implementation mistakes and how to avoid them
- How to test and validate your module before going live
What Is a Magento 2 Payment Module?
Custom payment requirements are one of the most common reasons merchants need to extend their Magento stores beyond off-the-shelf options. Whether you’re integrating a regional gateway, a B2B invoicing system, or a proprietary processor, the built-in payment options rarely cover every use case.
A Magento 2 payment module is the mechanism that connects your storefront to a payment service provider. It handles the full transaction lifecycle — from authorizing a charge to capturing funds, issuing refunds, and voiding orders. This guide walks through exactly how to create a Magento 2 payment module, covering every key file, configuration step, and common pitfall.
Understanding Payment Operations in Magento 2
What a Payment Module Controls
Before you start building, it helps to understand what operations a Magento 2 payment module can support.
| Operation | What It Does |
| Authorization | Blocks funds on the customer’s account without withdrawing |
| Capture | Withdraws the previously authorized amount |
| Sale (Auth + Capture) | Authorizes and captures in a single step |
| Refund | Returns funds to the customer’s account |
| Void | Cancels a pending authorization before capture |
Not every module needs to support all five. For example, a simple offline payment method like bank transfer only needs to handle order placement — no real-time gateway calls required.
When to Build Custom vs. Use an Extension
Most merchants can rely on Magento’s built-in gateways (Braintree, PayPal) or Marketplace extensions (Stripe, Authorize.Net). You need a custom Magento 2 payment module when your gateway has no existing extension, you need proprietary checkout logic, or you’re building for a region with local payment rails not covered by existing options.
Required File Structure to Create a Magento 2 Payment Module
Module Registration Files
Every Magento 2 module starts with two files. Without them, Magento won’t recognize the module at all.
app/code/Vendor/Payment/registration.php
php
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
‘Vendor_Payment’,
__DIR__
);
app/code/Vendor/Payment/etc/module.xml
xml
<?xml version="1.0"?>
<config xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance“
xsi:noNamespaceSchemaLocation=”urn:magento:framework:Module/etc/module.xsd“>
<module name=”Vendor_Payment“ setup_version=”1.0.0“>
<sequence>
<module name=”Magento_Sales“/>
<module name=”Magento_Payment“/>
<module name=”Magento_Checkout“/>
</sequence>
</module>
</config>
The sequence block ensures your module loads after Magento’s core payment and checkout modules — a step many developers skip that causes hard-to-debug errors.
Step-by-Step: How to Create a Magento 2 Payment Module
Step 1 — Configure Payment Settings in payment.xml
Create app/code/Vendor/Payment/etc/config.xml to define your method’s default configuration values:
xml
<?xml version="1.0"?>
<config xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance“
xsi:noNamespaceSchemaLocation=”urn:magento:module:Magento_Store:etc/config.xsd“>
<default>
<payment>
<vendor_simple>
<active>1</active>
<model>Vendor\Payment\Model\Payment\Simple</model>
<title>Simple Payment</title>
<payment_action>authorize</payment_action>
<order_status>pending</order_status>
</vendor_simple>
</payment>
</default>
</config>
Next, add the method to etc/payment.xml so Magento’s payment system recognizes it:
xml
<?xml version=”1.0″?>
<payment xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance“
xsi:noNamespaceSchemaLocation=”urn:magento:module:Magento_Payment:etc/payment.xsd“>
<groups>
<group id=”offline“>
<method name=”vendor_simple“/>
</group>
</groups>
</payment>
Step 2 — Build the Payment Model
The payment model is the core of your Magento payment module. Create Model/Payment/Simple.php:
php
<?php
namespace Vendor\Payment\Model\Payment;
use Magento\Payment\Model\Method\AbstractMethod;
class Simple extends AbstractMethod
{
protected $_code = ‘vendor_simple’;
protected $_isOffline = true;
protected $_canAuthorize = true;
protected $_canCapture = true;
protected $_canRefund = false;
protected $_canVoid = false;
}
For gateway-based methods that make live API calls, you’ll extend \Magento\Payment\Model\Method\Cc or use the newer GatewayCommand infrastructure instead.
Step 3 — Configure Dependency Injection
Magento 2 uses dependency injection extensively. Create etc/di.xml to wire your model into the payment adapter pool:
xml
<?xml version="1.0"?>
<config xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance“
xsi:noNamespaceSchemaLocation=”urn:magento:framework:ObjectManager/etc/config.xsd“>
<type name=”Magento\Payment\Model\Method\Factory“>
<arguments>
<argument name=”classMap“ xsi:type=”array“>
<item name=”vendor_simple“ xsi:type=”string“>Vendor\Payment\Model\Payment\Simple</item>
</argument>
</arguments>
</type>
</config>
Step 4 — Add the Frontend Layout Declaration
Magento 2 checkout is built on Knockout.js UI components. You need to inject your renderer into the checkout layout. Create view/frontend/layout/checkout_index_index.xml:
xml
<?xml version="1.0"?>
<page xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance“
xsi:noNamespaceSchemaLocation=”urn:magento:framework:View/Layout/etc/page_configuration.xsd“>
<body>
<referenceBlock name=”checkout.root“>
<arguments>
<argument name=”jsLayout“ xsi:type=”array“>
<item name=”components“ xsi:type=”array“>
<item name=”checkout“ xsi:type=”array“>
<item name=”children“ xsi:type=”array“>
<item name=”steps“ xsi:type=”array“>
<item name=”children“ xsi:type=”array“>
<item name=”billing-step“ xsi:type=”array“>
<item name=”children“ xsi:type=”array“>
<item name=”payment“ xsi:type=”array“>
<item name=”children“ xsi:type=”array“>
<item name=”renders“ xsi:type=”array“>
<item name=”children“ xsi:type=”array“>
<item name=”vendor-payment“ xsi:type=”array“>
<item name=”component“ xsi:type=”string“>Vendor_Payment/js/view/payment/simple</item>
<item name=”methods“ xsi:type=”array“>
<item name=”vendor_simple“ xsi:type=”array“>
<item name=”isBillingAddressRequired“ xsi:type=”boolean“>true</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</argument>
</arguments>
</referenceBlock>
</body>
</page>
Step 5 — Create the JavaScript View and Renderer
Create the parent JS view at view/frontend/web/js/view/payment/simple.js:
javascript
define([
‘uiComponent’,
‘Magento_Checkout/js/model/payment/renderer-list’
], function (Component, rendererList) {
‘use strict’;
rendererList.push({
type: ‘vendor_simple’,
component: ‘Vendor_Payment/js/view/payment/method-renderer/simple-method’
});
return Component.extend({});
});
Then create the renderer at view/frontend/web/js/view/payment/method-renderer/simple-method.js:
javascript
define([
‘Magento_Checkout/js/view/payment/default’
], function (Component) {
‘use strict’;
return Component.extend({
defaults: {
template: ‘Vendor_Payment/payment/simple’
},
getCode: function () {
return ‘vendor_simple’;
}
});
});
Step 6 — Create the Checkout Template
The HTML template controls what the customer sees on the payment step. Create view/frontend/web/template/payment/simple.html:
html
<div class="payment-method" data-bind="css: {'_active': (getCode() == isChecked())}">
<div class=”payment-method-title field choice“>
<input type=”radio“
name=”paymentMethod“
class=”radio“
data-bind=”attr: {‘id‘: getCode()}, value: getCode(), checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()“ />
<label data-bind=”attr: {‘for‘: getCode()}“ class=”label“>
<span data-bind=”text: getTitle()“></span>
</label>
</div>
<div class=”payment-method-content“>
<div class=”payment-method-billing-address“></div>
<div class=”checkout-agreements-block“></div>
<div class=”actions-toolbar“>
<div class=”primary“>
<button class=”action primary checkout“
type=”submit“
data-bind=”click: placeOrder, attr: {title: $t(‘Place Order‘)}“>
<span data-bind=”text: $t(‘Place Order‘)“></span>
</button>
</div>
</div>
</div>
</div>
Enabling and Testing Your Module
Module Activation Commands
Run these commands after creating all files:
bash
php bin/magento module:enable Vendor_Payment
php bin/magento setup:upgrade
php bin/magento setup:di:compile
php bin/magento cache:clean
Common Errors and Fixes
| Error | Likely Cause | Fix |
| Payment method doesn’t appear at checkout | Missing layout XML or JS renderer | Check checkout_index_index.xml and clear cache |
| Module not recognized | Missing registration.php | Verify file path and namespace |
| DI compilation fails | Incorrect class references in di.xml | Check namespaces match actual file paths |
| JS errors in browser console | Template path mismatch | Confirm template path in simple-method.js matches actual file |
Cache Management During Development
A critical workflow tip: always disable the Full Page Cache and Block HTML Output caches during development. You can do this from Admin > System > Cache Management. Also delete the var/cache, var/page_cache, and pub/static/frontend directories when JS or layout changes aren’t reflecting.
Implementation Tips to Avoid Common Mistakes
What Developers Get Wrong
Most problems when you create a Magento 2 payment module come from three areas: module sequencing, UI component inheritance, and cache issues.
Your module must declare Magento_Checkout in its sequence in module.xml. Skipping this causes intermittent checkout failures that are difficult to trace.
Payment renderers must extend Magento_Checkout/js/view/payment/default, not be written from scratch. This base component handles order placement, validation, and communication with the checkout model.
Never disable cache globally while working with Magento classes. Instead, selectively disable only Layout, Block HTML Output, and Full Page Cache. Keep the Config and DI caches active to ensure your dependency injection and model configurations load correctly.
Developer Mode vs. Production
Always build and test a custom Magento payment module in developer mode (php bin/magento deploy:mode:set developer). This disables caching of static files and enables detailed error reporting — both essential for catching JS and PHP errors during development.
Payment Module Configuration in the Admin Panel
System Configuration Setup
For your payment method to appear in Admin > Stores > Configuration > Sales > Payment Methods, add etc/adminhtml/system.xml:
xml
<?xml version="1.0"?>
<config xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance“
xsi:noNamespaceSchemaLocation=”urn:magento:module:Magento_Config:etc/system_file.xsd“>
<system>
<section id=”payment“>
<group id=”vendor_simple“ translate=”label“ type=”text“ sortOrder=”100“ showInDefault=”1“ showInWebsite=”1“ showInStore=”1“>
<label>Simple Payment Method</label>
<field id=”active“ translate=”label“ type=”select“ sortOrder=”1“ showInDefault=”1“ showInWebsite=”1“ showInStore=”0“>
<label>Enabled</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id=”title“ translate=”label“ type=”text“ sortOrder=”2“ showInDefault=”1“ showInWebsite=”1“ showInStore=”1“>
<label>Title</label>
</field>
</group>
</section>
</system>
</config>
This gives merchants control over enabling the method and setting a display title without touching code.
File Structure Overview
| File Path | Purpose |
| etc/module.xml | Declares module name and dependencies |
| etc/config.xml | Sets default configuration values |
| etc/payment.xml | Registers method with Magento’s payment system |
| etc/di.xml | Wires model into the dependency injection container |
| Model/Payment/Simple.php | Defines payment logic and supported operations |
| view/frontend/layout/checkout_index_index.xml | Injects renderer into checkout layout |
| view/frontend/web/js/view/payment/simple.js | Registers renderer with Magento’s renderer list |
| view/frontend/web/js/view/payment/method-renderer/simple-method.js | Handles payment selection and order placement |
| view/frontend/web/template/payment/simple.html | Renders the payment option at checkout |
| etc/adminhtml/system.xml | Adds admin configuration fields |
Key Takeaways
- A Magento 2 payment module requires both backend (PHP model) and frontend (JS renderer + HTML template) components working together.
- Module sequencing in module.xml must include Magento_Checkout — missing this is one of the most common causes of checkout failures.
- Always extend Magento_Checkout/js/view/payment/default for your JS renderer; writing a renderer from scratch breaks core checkout functionality.
- Disable only selective caches during development, not all caches — full cache disabling can mask real dependency injection issues.
- Test each operation your module claims to support (authorize, capture, refund) in a staging environment before deploying.
Conclusion
Building a Magento 2 payment module from scratch takes careful attention to both the PHP backend and the Knockout.js frontend — but the structure is consistent and repeatable once you understand it. The most common failures come from skipped sequencing, incorrect renderer inheritance, and poor cache management during development, not from the payment logic itself.
If your team needs support building or integrating a custom Magento payment module, Folio3’s Magento development team can help you get it done right. Talk to our experts to scope your project.
For related reading, check out our guides on Magento POS integration and how Magento compares to other enterprise platforms.
FAQs
What Files Do I Need to Create a Magento 2 Payment Module?
At minimum: registration.php, module.xml, config.xml, payment.xml, di.xml, a PHP payment model, a layout XML file, two JavaScript files, and an HTML template. Admin configuration requires system.xml as well.
How Do I Add a Custom Payment Method to Magento 2 Checkout?
Register the method in payment.xml, create a JS view and renderer that extend Magento’s default checkout components, and inject them via checkout_index_index.xml. Run setup:upgrade and clear cache after changes.
Can I Create a Magento 2 Payment Module Without Gateway API Calls?
Yes. Extend AbstractMethod with $_isOffline = true to create an offline payment method (like bank transfer or purchase order) that doesn’t make live API requests. This is simpler to build and test.
How Long Does It Take to Build a Custom Magento Payment Module?
A basic offline module can be built in a few hours. A fully integrated gateway module with authorization, capture, refund, and admin configuration typically takes 2–5 days depending on the gateway’s API complexity.
What Is the Difference Between Authorize and Capture in Magento 2?
Authorization blocks funds on the customer’s account without withdrawing them. Capture withdraws the previously authorized amount. Most online stores use “Authorize and Capture” (Sale) simultaneously, while B2B stores often separate them to authorize on order and capture on shipment.