Integrating custom payment gateways is essential for expanding checkout capabilities and supporting regional preferences. Shopware 6, particularly in version 6.7, provides a mature, type-safe architecture that simplifies connecting external providers while enforcing security and performance standards. This guide details how to implement payment integrations via API using Shopware's extension framework, focusing on modern development practices compatible with Shopware 6.7+.

The Payment Handler Architecture

In Shopware 6.7, custom payments are built around the PaymentMethodHandlerInterface. The architecture separates the checkout flow from gateway logic. Your handler acts as the orchestrator: it receives order data, validates context, constructs API requests to the provider, and maps the response to a result that updates the transaction entity.

Shopware 6.7 reinforces this pattern by requiring strict typing and immutable configurations where possible. The framework utilizes PSR-18 HTTP clients, ensuring your integrations are portable and testable.

Implementing the Handler in Shopware 6.7

Below is a complete example of a synchronous payment handler. This code demonstrates Dependency Injection (DI), secure request construction, and proper error handling compatible with Shopware 6.7+.

declare(strict_types=1);

namespace YourVendor\\YourShopwareExtension\\Payment\\Handler;

use Psr\\Http\\Client\\ClientInterface;
use Psr\\Http\\Message\\ResponseInterface;
use Shopware\\Core\\Checkout\\Payment\\Cart\\Order\\Entity\\OrderTransactionEntity;
use Shopware\\Core\\Checkout\\Payment\\PaymentException;
use Shopware\\Core\\Checkout\\Payment\\SalesChannel\\PaymentMethodHandlerInterface;
use Shopware\\Core\\Checkout\\Payment\\SalesChannel\\PaymentResult;
use Shopware\\Core\\Framework\\Http\\ClientResponseTrait;
use Symfony\\Contracts\\Translation\\TranslatorInterface;
use Throwable;

final class CustomGatewayPaymentMethodHandler implements PaymentMethodHandlerInterface
{
    use ClientResponseTrait;

    public function __construct(
        private readonly ClientInterface $httpClient,
        private readonly string $apiKey,
        private readonly string $baseUrl,
        private readonly TranslatorInterface $translator
    ) {}

    /**
     * @param array<string, mixed> $payload
     */
    public function finalize(OrderTransactionEntity $transaction, array $payload): PaymentResult
    {
        $orderId = (string) $transaction->getOrderId();
        $amount = $transaction->getAmount()->getTotalPrice();

        try {
            // Construct payload for gateway API
            $requestPayload = [
                'order_reference' => $orderId,
                'amount' => $amount,
                'currency' => $transaction->getOrder()->getCurrency()->getIsoCode(),
                'token' => $payload['gateway_token'] ?? null,
            ];

            // Execute API call via PSR-18 Client
            /** @var ResponseInterface $response */
            $response = $this->httpClient->request('POST', sprintf('%s/charges', $this->baseUrl), [
                'json' => $requestPayload,
                'headers' => [
                    'Authorization' => 'Bearer ' . $this->apiKey,
                    'Content-Type' => 'application/json',
                    'Idempotency-Key' => md5($orderId . $amount . microtime(true)), // Prevent double charges
                ],
            ]);

            $data = $this->getJsonArrayFromResponse($response);

            if ($data['status'] === 'approved') {
                return new PaymentResult(
                    true,
                    $data['provider_transaction_id'],
                    [
                        'provider_payment_id' => $data['id'],
                        'raw_response' => json_encode($data),
                    ]
                );
            }

            // Map gateway errors to Shopware UI messages
            throw PaymentException::paymentDeclined(
                $orderId,
                (string) $transaction->getId(),
                $this->translator->trans('checkout.paymentError', ['%message%' => $data['error_message'] ?? 'Unknown error'])
            );

        } catch (PaymentException $e) {
            throw $e; // Re-throw validation/payment exceptions directly
        } catch (Throwable $e) {
            // Handle connectivity or parsing errors
            throw new \Exception(
                $this->translator->trans('checkout.paymentServiceUnavailable'),
                0,
                $e
            );
        }
    }
}

Key Implementation Details

  • Dependency Injection: The handler constructor accepts the HTTP client and configuration parameters. In Shopware 6.7, using readonly properties ensures immutability after instantiation, which is crucial for concurrent request safety. Configuration values like API keys should be injected via system parameters, never hardcoded.
  • HTTP Client: We utilize ClientInterface, which resolves to Shopware's underlying HTTP client implementation. This allows you to override the client globally or per-handler for custom SSL configurations, proxies, or timeout limits.
  • Idempotency: The example includes an Idempotency-Key header. When calling payment APIs, always generate a unique key per request (e.g., using order ID and timestamp) to prevent double charges if the gateway receives duplicate requests due to network retries.
  • Error Handling: PaymentException::paymentDeclined is used to communicate decline reasons back to the checkout flow. Other exceptions are wrapped to protect sensitive stack traces from leaking to the frontend while ensuring the transaction fails safely.

DTOs and Response Validation

Shopware 6.7 encourages type safety throughout the stack. When parsing API responses, avoid using raw arrays for business logic. Instead, define a Data Transfer Object (DTO) that matches the gateway's schema. Use libraries like Symfony Serializer or custom validation to ensure the response structure is correct before extracting values. This prevents runtime errors caused by API schema drift and improves code maintainability.

Asynchronous Payments and Webhooks

Many modern gateways operate asynchronously. For these flows, extend Shopware\Core\Checkout\Payment\SalesChannel\AbstractAsyncPaymentHandler. Your handler should return a response that redirects the user to the provider or returns a JSON payload containing an approval URL.

When the payment provider confirms the transaction via webhook, Shopware exposes an endpoint (typically /custom/sw-gateway-webhook) where you must validate the request signature, verify the amount, and update the order state using the Order Repository. Ensure your webhook handler is idempotent to handle duplicate notifications gracefully.

Service Registration

To activate the integration, register the service in services.xml. You must tag the handler with shopware.payment.method_handler and map it to the Payment Method ID created in the Shopware Administration.

<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services">
    <services>
        <!-- Configuration parameters should be defined in SystemConfiguration -->
        <service id="YourVendor\\YourShopwareExtension\\Payment\\Handler\\CustomGatewayPaymentMethodHandler">
            <argument type="service" id="Psr\\Http\\Client\\ClientInterface"/>
            <argument>%shopware.custom_gateway.api_key%</argument>
            <argument>%shopware.custom_gateway.base_url%</argument>
            <argument type="service" id="translator"/>
            
            <!-- Link to the Payment Method ID defined in the Administration -->
            <tag name="shopware.payment.method_handler" method-id="custom-gateway-method-id"/>
        </service>
    </services>
</container>
  • System Configuration: Use Shopware's SystemConfiguration class to allow admins to input API keys and URLs in the backend. Store these secrets in sw_sys_config using encryption masks where supported.
  • Method ID: The method-id attribute in the tag must exactly match the technical_name or UUID of the Payment Method entity in Shopware.

Security and Compliance

Connecting to payment gateways requires strict adherence to security best practices:

  1. Tokenization: Never store full credit card numbers or CVVs. Rely on the gateway's tokenization flow. Store only provider IDs and transaction references.
  2. Logging Sanitization: Implement a PSR-3 logger middleware or custom handler that masks sensitive fields in API requests and responses before they are written to Shopware's log files.
  3. HTTPS Enforcement: Ensure all outbound connections use TLS 1.2 or higher. Validate certificates strictly to prevent man-in-the-middle attacks.
  4. Testing: Write unit tests mocking ClientInterface to simulate success, decline, and network failure scenarios. Use Shopware's test infrastructure to verify that order states transition correctly and that exceptions are handled as expected.

Conclusion

Shopware 6.7 provides a powerful, secure foundation for payment integrations. By leveraging the PaymentMethodHandlerInterface, strict dependency injection, and PSR-compliant HTTP clients, you can build robust connections to any modern payment gateway. Adhering to these architectural patterns ensures your extension remains maintainable, PCI-compliant, and fully compatible with the Shopware ecosystem. Whether implementing synchronous charges or asynchronous webhook flows, a disciplined approach to API integration will deliver a seamless checkout experience for your merchants and customers.