Extending your Shopware store with a custom payment method has never been more modular or secure. With the release of Shopware 6.7, the platform introduced a complete architectural overhaul of the checkout flow. The legacy AbstractPaymentHandler class was officially deprecated in favor of the Payment Provider API v2, which decouples payment execution from transaction management, aligns with Symfony service patterns, and enforces strict state machine boundaries. This guide walks you through building a production-ready custom payment plugin tailored specifically for Shopware 6.7 and beyond.

Understanding the 6.7 Payment Architecture Shift

Before diving into code, it’s crucial to grasp what changed in 6.7. Previously, providers manipulated OrderTransactionEntity directly, leading to race conditions during async confirmations and tangled rollback logic. Shopware 6.7 introduces a phase-driven lifecycle: START, PROCESS, and FINISH. Your plugin now defines how a payment interacts with an external gateway, while the framework handles persistence, state transitions, and compensation via the internal TransactionBehaviorFactory. This separation means you never directly update database rows during checkout. Instead, you return context objects, throw structured exceptions, or delegate to Symfony services. The result is a predictable, testable, and secure payment ecosystem that scales with modern e-commerce requirements like 3D Secure, split settlements, and webhook-driven confirmations.

Step 1: Plugin Skeleton & Service Container Registration

Every Shopware plugin must declare its structure in plugin.xml and register services properly. Create your directory under src/Plugins/CustomPaymentProvider/ and define the service container configuration:

<!-- src/Plugins/CustomPaymentProvider/services/services.xml -->
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services https://schema.symfony.com/dic/services/services-1.0.xsd">
    <service id="CustomPaymentProvider\Payment\CustomPaymentProvider"
             class="CustomPaymentProvider\Payment\CustomPaymentProvider">
        <tag name="shopware.payment.provider"/>
    </service>
</container>

The shopware.payment.provider tag is mandatory. During kernel boot, Shopware’s compiler pass collects all tagged services and registers them in the payment routing pipeline. Ensure your namespace matches your directory layout, and reference this configuration file in your plugin’s Services::class method. Missing this step results in silent service failures during checkout.

Step 2: Implementing the PaymentProviderInterface v2

Your core logic resides in a class implementing Shopware\Core\Checkout\Payment\PaymentProviderInterface. Each phase serves a distinct purpose:

namespace CustomPaymentProvider\Payment;

use Shopware\Core\Checkout\Payment\Exception\ProcessPaymentException;
use Shopware\Core\Checkout\Payment\PaymentContextException;
use Shopware\Core\Checkout\Payment\SalesChannel\PaymentContextProcessor;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\HttpFoundation\Request;

#[Package('checkout')]
class CustomPaymentProvider implements \Shopware\Core\Checkout\Payment\PaymentProviderInterface
{
    public function start(PaymentContextProcessor $contextProcessor, SalesChannelContext $salesChannelContext): void
    {
        // Phase 1: Prepare payment intent, generate external reference ID, or store gateway session.
        // No state mutations allowed. Use this to call your provider's create-session endpoint.
    }

    public function process(SalesChannelContext $salesChannelContext): void
    {
        // Phase 2: Triggered on checkout confirmation. Handle sync authorization or return immediately for async flows.
        // Validate amount, currency, and fraud rules here. Throw PaymentContextException on rejection.
    }

    public function finish(Request $request, SalesChannelContext $salesChannelContext): array
    {
        // Phase 3: Handle customer redirect back from gateway or process webhook payload.
        // Verify signatures, match external transaction ID, and return template context.
        return [];
    }
}

Notice the strict boundary between phases. start() prepares data, process() initiates payment flow, and finish() confirms it. Shopware 6.7 automatically wraps these calls in safe database transactions. If you need to update transaction state based on your gateway’s response, use TransactionBehaviorFactory instead of direct CRUD operations. Throw PaymentContextException for user-facing validation errors (e.g., insufficient funds, card decline), and let the framework handle rollback gracefully.

Step 3: Storefront Configuration & Data Persistence

Merchants must configure API credentials, environment toggles, and display names. Shopware 6.7 persists these values in the payment_method table under a JSON configuration column. Register a form via the storefront service tag:

<service id="CustomPaymentProvider\Form\CustomPaymentFormType" class="CustomPaymentProvider\Form\CustomPaymentFormType">
    <tag name="shopware.storefront.form" type="CustomPaymentProvider" />
</service>

Your form class should extend Shopware\Core\Framework\DataAbstractionLayer\FieldCollection or leverage Symfony’s FormComponent to expose fields like apiKey, secretKey, and testMode. Shopware automatically binds these to your payment method instance during configuration. Always sanitize input at the controller layer, and never expose secrets in frontend templates. Use the configuration store service if you prefer centralised credential management across multiple plugins.

Step 4: Secure Webhook Handling & State Transitions

Async gateways require secure callback routing. Create a Symfony controller that validates incoming payloads and updates order states safely:

namespace CustomPaymentProvider\Controller;

use Shopware\Core\Checkout\Order\SalesChannel\OrderServiceInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

#[Route(path: '/custom-payment-webhook', name: 'api_custom_payment_webhook', defaults: ['_routeScope' => ['api']])]
class PaymentWebhookController extends AbstractController
{
    public function __invoke(Request $request, OrderServiceInterface $orderService): Response
    {
        // 1. Verify HMAC signature & replay protection
        // 2. Decode JSON payload & extract external_reference_id
        // 3. Match transaction via SalesChannelContext or OrderEntity repository
        // 4. Update status using Shopware's built-in state machine
        return new Response('ACK', 200);
    }
}

Never trust raw POST data. Implement idempotency checks, timeout handling, and cryptographic verification. Shopware 6.7 provides OrderTransactionStateHandler for safe status transitions that respect your custom payment rule engine. Log all webhook attempts with correlation IDs to simplify debugging in production.

Step 5: Testing, Validation & Production Readiness

Use PHPUnit with mocked SalesChannelContextFactory and PaymentContextProcessor to validate each phase independently. Inject test providers via dependency injection containers, and verify exception throwing paths rigorously. Avoid hardcoding credentials; use Shopware’s parameter bag or configuration store for environment-specific values. Clear cache after plugin installation (bin/plugin refresh), verify payment method visibility in storefront rules, and follow PSR-12 standards for ecosystem compatibility. Monitor transaction logs via bin/log to catch silent failures during processing.

Conclusion

Building a custom payment plugin in Shopware 6.7+ is now more disciplined, secure, and aligned with modern Symfony patterns. By embracing the Payment Provider v2 API, you separate concerns effectively, reduce boilerplate, and future-proof your integration against framework updates. Start by mapping your gateway’s lifecycle to START, PROCESS, and FINISH, implement robust webhook validation, and test exhaustively across checkout contexts. The Shopware 6.7 payment ecosystem rewards clean architecture—so build yours with precision, document your contracts clearly, and deliver a seamless checkout experience for merchants and shoppers alike.