Unterschiede in Architektur und Webhooks im Detail
Eine PayPal- und Adyen-Integration in Magento 2 ist kein Austausch zweier gleichwertiger Zahlungsmethoden: clientseitige Order-Erstellung trifft auf serverseitige Sessions, zertifikatsbasierte Signaturen auf HMAC-Notifications. Dieser Artikel vergleicht beide Architekturen bis auf Command-Klassen-Ebene.
Inhaltsverzeichnis
- 1. Warum die Architektur-Unterschiede zwischen PayPal und Adyen entscheidend sind
- 2. PayPal-Architektur: REST API, Smart Payment Buttons und clientseitige Order-Erstellung
- 3. Adyen-Architektur: Drop-in Component, Sessions API und serverseitige Payment-Erstellung
- 4. Webhook-Verarbeitung bei PayPal: IPN, Webhook-Events und Signaturprüfung
- 5. Webhook-Verarbeitung bei Adyen: HMAC-Signatur und idempotente Verarbeitung
- 6. Magento Payment-Method-Facade: eigene Gateway-Command-Klassen für beide Provider
- 7. Unterschiede in der Order-State-Machine: Pending, Authorized, Captured
- 8. Refunds und Captures: API-Unterschiede bei Teilerstattung und Teilkapturierung
- 9. Sicherheits- und PCI-Aspekte: Tokenisierung und 3D Secure 2
- 10. Zusammenfassung
- 11. FAQ
1. Warum die Architektur-Unterschiede zwischen PayPal und Adyen bei Magento-Integrationen entscheidend sind
Wer in einem Magento-2-Projekt vor der Entscheidung steht, PayPal oder Adyen anzubinden, unterschätzt häufig, wie unterschiedlich beide Anbieter technisch aufgebaut sind. Eine PayPal- und Adyen-Integration ist keine Frage zweier austauschbarer Zahlungsmethoden mit demselben Interface, sondern zweier grundverschiedener Architekturen: PayPal setzt auf clientseitige Order-Erstellung über die Smart Payment Buttons, Adyen auf eine serverseitige Sessions API mit dem Drop-in Component. Diese Unterschiede wirken sich direkt auf Order-Erstellung, Webhook-Verarbeitung und die Payment-Method-Facade in Magento aus, lange bevor die erste Testtransaktion läuft.
Die Konsequenz für Entwicklerteams: Ein generischer Zahlungsgateway-Adapter, der beide Provider mit derselben internen Logik bedient, führt fast immer zu Fehlern in Edge Cases, etwa bei Teilerstattungen, doppelten Webhook-Zustellungen oder abweichenden Order-Status. Eine saubere PayPal-Integration braucht andere Command-Klassen, andere Webhook-Controller und eine andere State-Machine-Abbildung als eine Adyen-Integration, selbst wenn am Ende beide über dieselbe Magento Payment-Method-Facade angesprochen werden.
Dieser Artikel vergleicht beide Architekturen Schritt für Schritt: von der Order-Erstellung über die Webhook-Signaturprüfung bis zur State-Machine und den PCI-relevanten Aspekten der Tokenisierung. Ziel ist ein technisches Verständnis, das eine fundierte Entscheidung für die passende PayPal- und Adyen-Integration im jeweiligen Projekt ermöglicht, statt beide Anbieter über einen Kamm zu scheren.
2. PayPal-Architektur: REST API, Smart Payment Buttons und clientseitige Order-Erstellung
Die PayPal-Integration basiert auf der PayPal REST API v2, konkret den Endpunkten der Orders API. Der zentrale Baustein im Frontend ist der JavaScript SDK, der die Smart Payment Buttons rendert. Beim Klick auf den Button ruft der Browser direkt die Methode createOrder auf, die intern einen POST an /v2/checkout/orders absetzt und eine PayPal-Order-ID zurückliefert, noch bevor Magento serverseitig überhaupt eine eigene Order angelegt hat. Diese clientseitige Order-Erstellung unterscheidet PayPal grundlegend von serverzentrierten Zahlungsarchitekturen.
Erst nach der Genehmigung durch den Kunden im PayPal-Popup oder -Redirect ruft das Frontend approve und anschließend serverseitig capture oder authorize auf. Für Magento bedeutet das: Die Backend-Integration muss die im Browser erzeugte PayPal-Order-ID entgegennehmen, in additional_information am Payment speichern und erst dann die eigentliche Order-Erstellung sowie Autorisierung serverseitig anstoßen. Diese zeitliche Verzögerung zwischen Order-Erstellung im PayPal-System und Order-Erstellung in Magento ist eine der häufigsten Fehlerquellen bei einer PayPal-Integration, insbesondere wenn der Kunde den Checkout-Tab schließt, bevor die serverseitige Bestätigung abgeschlossen ist.
Für die REST-API-Kommunikation selbst nutzt PayPal OAuth2 Client-Credentials mit Client-ID und Secret, die für Magento typischerweise über ein eigenes ConfigProvider-Modul im system.xml verwaltet werden. Die Architektur bleibt damit vergleichsweise schlank: kein serverseitiges Session-Objekt vor dem Checkout, dafür eine engere Kopplung zwischen Frontend-JavaScript und Backend-API-Aufrufen, die bei jeder PayPal-Integration sauber synchronisiert werden muss.
3. Adyen-Architektur: Drop-in Component, Sessions API und serverseitige Payment-Erstellung
Die Adyen-Integration verfolgt den entgegengesetzten Ansatz. Bevor im Frontend überhaupt eine Zahlungsoberfläche erscheint, erzeugt Magento serverseitig über die Adyen Checkout Sessions API eine Payment Session, inklusive Betrag, Währung und Referenz. Erst das Ergebnis dieses serverseitigen Aufrufs, insbesondere das Feld sessionData, wird an das Drop-in Component im Browser übergeben, das daraufhin die passenden Zahlungsmethoden rendert.
Diese serverseitige Payment-Erstellung hat einen entscheidenden architektonischen Vorteil gegenüber PayPals clientseitigem Modell: Betrag und Referenz werden nie im Browser manipulierbar generiert, sondern ausschließlich serverseitig festgelegt, bevor die Session überhaupt existiert. Das folgende Beispiel zeigt einen schlanken Client für die Sessions API, wie er in einer Adyen-Integration typischerweise als eigene Service-Klasse gekapselt wird.
<?php
declare(strict_types=1);
namespace Mironsoft\Payment\Model\Adyen;
use Adyen\Client;
use Adyen\Service\Checkout;
use Magento\Framework\Exception\LocalizedException;
/**
* Thin wrapper around the Adyen Checkout Sessions API.
*/
final class SessionsClient
{
public function __construct(
private readonly Client $adyenClient,
private readonly string $merchantAccount,
) {
}
/**
* Creates a payment session server-side before the Drop-in Component renders.
*
* @param string $orderIncrementId Magento order increment id used as Adyen reference.
* @param int $amountMinorUnits Order total in minor currency units (e.g. cents).
* @param string $currencyCode ISO 4217 currency code.
* @param string $returnUrl Redirect target after payment method flows.
* @return array<string, mixed> Session data including sessionId and sessionData.
* @throws LocalizedException
*/
public function createSession(
string $orderIncrementId,
int $amountMinorUnits,
string $currencyCode,
string $returnUrl,
): array {
$checkout = new Checkout($this->adyenClient);
try {
return $checkout->sessions([
'merchantAccount' => $this->merchantAccount,
'reference' => $orderIncrementId,
'amount' => [
'value' => $amountMinorUnits,
'currency' => $currencyCode,
],
'returnUrl' => $returnUrl,
'channel' => 'Web',
]);
} catch (\Adyen\AdyenException $exception) {
throw new LocalizedException(__('Adyen session could not be created.'), $exception);
}
}
}
Nach Abschluss der Zahlungsmethode im Drop-in Component sendet Adyen das Ergebnis über ein Redirect oder eine asynchrone Notification zurück an Magento. Die eigentliche Bestätigung erfolgt jedoch nie ausschließlich über den Redirect, sondern immer zusätzlich über die serverseitige Notification, was die Adyen-Architektur robuster gegenüber abgebrochenen Browser-Sessions macht als die stärker frontend-getriebene PayPal-Integration.
4. Webhook-Verarbeitung bei PayPal: IPN- und Webhook-Events, Signaturprüfung, relevante Event-Typen
PayPal unterscheidet historisch zwischen dem älteren IPN-Mechanismus (Instant Payment Notification) und den moderneren PayPal Webhooks, die auf der REST API aufsetzen. Für neue PayPal-Integrationen sind ausschließlich Webhooks relevant, IPN gilt als Legacy und sollte in neuen Magento-Projekten nicht mehr implementiert werden. Ein PayPal-Webhook liefert pro HTTP-Request genau ein Event, identifiziert über das Feld event_type, etwa PAYMENT.CAPTURE.COMPLETED, PAYMENT.CAPTURE.DENIED oder PAYMENT.CAPTURE.REFUNDED.
Die Signaturprüfung eines eingehenden Webhooks erfolgt über mehrere HTTP-Header: Paypal-Transmission-Id, Paypal-Transmission-Time, Paypal-Cert-Url, Paypal-Auth-Algo und Paypal-Transmission-Sig. PayPal signiert die Nutzlast mit einem privaten Schlüssel, dessen öffentliches Zertifikat über die in Paypal-Cert-Url angegebene URL abgerufen und geprüft werden muss, entweder lokal oder über den Verify-Webhook-Signature-Endpunkt der REST API. Ohne diese Prüfung akzeptiert ein Magento-Controller theoretisch jede beliebige Nutzlast als echtes PayPal-Event, ein erhebliches Sicherheitsrisiko für jede produktive PayPal-Integration.
Der folgende Controller zeigt eine schlanke Umsetzung, die Signatur-Header extrahiert, die Prüfung an einen eigenen Verifier delegiert und erst danach das Event anhand des event_type verarbeitet.
<?php
declare(strict_types=1);
namespace Mironsoft\Payment\Controller\Webhook;
use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\App\CsrfAwareActionInterface;
use Magento\Framework\App\Request\InvalidRequestException;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Framework\Controller\ResultInterface;
use Mironsoft\Payment\Model\Paypal\WebhookSignatureVerifier;
use Psr\Log\LoggerInterface;
/**
* Receives PayPal webhook events and verifies their transmission signature before dispatching.
*/
final class Paypal implements HttpPostActionInterface, CsrfAwareActionInterface
{
public function __construct(
private readonly RequestInterface $request,
private readonly JsonFactory $resultJsonFactory,
private readonly WebhookSignatureVerifier $signatureVerifier,
private readonly LoggerInterface $logger,
) {
}
/**
* Verifies the transmission signature and processes the webhook event payload.
*
* @return ResultInterface
*/
public function execute(): ResultInterface
{
$result = $this->resultJsonFactory->create();
$body = (string) $this->request->getContent();
$headers = [
'transmissionId' => (string) $this->request->getHeader('Paypal-Transmission-Id'),
'transmissionTime' => (string) $this->request->getHeader('Paypal-Transmission-Time'),
'certUrl' => (string) $this->request->getHeader('Paypal-Cert-Url'),
'authAlgo' => (string) $this->request->getHeader('Paypal-Auth-Algo'),
'transmissionSig' => (string) $this->request->getHeader('Paypal-Transmission-Sig'),
];
if (!$this->signatureVerifier->isValid($headers, $body)) {
$this->logger->warning('PayPal webhook signature verification failed.');
return $result->setHttpResponseCode(400)->setData(['status' => 'invalid_signature']);
}
/** @var array<string, mixed> $event */
$event = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
$eventType = (string) ($event['event_type'] ?? '');
// Relevant event types for order and capture reconciliation
match ($eventType) {
'PAYMENT.CAPTURE.COMPLETED', 'PAYMENT.CAPTURE.DENIED', 'PAYMENT.CAPTURE.REFUNDED' => $this->logger->info(
sprintf('Processing PayPal event %s', $eventType)
),
default => $this->logger->info(sprintf('Ignoring unhandled PayPal event %s', $eventType)),
};
return $result->setData(['status' => 'ok']);
}
/**
* Disables CSRF validation for this webhook endpoint since PayPal cannot supply a form key.
*
* @param RequestInterface $request
* @return InvalidRequestException|null
*/
public function createCsrfValidationException(RequestInterface $request): ?InvalidRequestException
{
return null;
}
/**
* @param RequestInterface $request
* @return bool
*/
public function validateForCsrf(RequestInterface $request): bool
{
return true;
}
}
5. Webhook-Verarbeitung bei Adyen: HMAC-Signatur, notification items und idempotente Verarbeitung
Adyen strukturiert Webhooks grundlegend anders als PayPal. Ein einzelner HTTP-Request kann ein Array namens notificationItems enthalten, das mehrere NotificationRequestItem-Objekte gleichzeitig transportiert, etwa wenn Autorisierung und ein sofortiger Capture im selben Batch gemeldet werden. Eine Adyen-Integration muss deshalb pro Request eine Schleife über alle Items durchlaufen, statt wie bei PayPal von genau einem Event pro Aufruf auszugehen.
Die Signaturprüfung erfolgt pro Item über HMAC-SHA256, nicht über ein Zertifikat wie bei PayPal. Der Wert steckt im Feld additionalData.hmacSignature jedes einzelnen NotificationRequestItem und wird gegen einen zuvor im Adyen Customer Area hinterlegten, geteilten HMAC-Key geprüft. Da der Schlüssel symmetrisch ist, genügt eine lokale Berechnung ohne externen Zertifikats-Abruf, was die HMAC-Prüfung tendenziell einfacher zu implementieren macht als PayPals zertifikatsbasierte Signaturprüfung, aber ebenso zwingend erforderlich für jede sichere Adyen-Integration.
Weil Adyen dieselbe Notification bei ausbleibender Bestätigung wiederholt zustellen kann, muss die Verarbeitung zwingend idempotent sein: Eine bereits verarbeitete Kombination aus pspReference und eventCode darf kein zweites Mal zu einer doppelten Buchung führen. Der folgende Controller prüft die HMAC-Signatur pro Item und führt zusätzlich ein Idempotenz-Register, bevor ein Event tatsächlich verarbeitet wird.
<?php
declare(strict_types=1);
namespace Mironsoft\Payment\Controller\Webhook;
use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\App\CsrfAwareActionInterface;
use Magento\Framework\App\Request\InvalidRequestException;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Framework\Controller\ResultInterface;
use Mironsoft\Payment\Model\Adyen\HmacValidator;
use Mironsoft\Payment\Model\Adyen\ProcessedNotificationRegistry;
use Psr\Log\LoggerInterface;
/**
* Receives Adyen notification webhooks and verifies the HMAC signature per item.
*/
final class Adyen implements HttpPostActionInterface, CsrfAwareActionInterface
{
public function __construct(
private readonly RequestInterface $request,
private readonly JsonFactory $resultJsonFactory,
private readonly HmacValidator $hmacValidator,
private readonly ProcessedNotificationRegistry $processedRegistry,
private readonly LoggerInterface $logger,
) {
}
/**
* Iterates all notification items in the payload and processes each idempotently.
*
* @return ResultInterface
*/
public function execute(): ResultInterface
{
$result = $this->resultJsonFactory->create();
$body = (string) $this->request->getContent();
/** @var array{notificationItems: array<int, array{NotificationRequestItem: array<string, mixed>}>} $payload */
$payload = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
foreach ($payload['notificationItems'] as $wrapper) {
$item = $wrapper['NotificationRequestItem'];
$hmacSignature = (string) ($item['additionalData']['hmacSignature'] ?? '');
if (!$this->hmacValidator->isValid($item, $hmacSignature)) {
$this->logger->warning('Adyen notification item failed HMAC validation.');
return $result->setHttpResponseCode(401)->setData(['notificationResponse' => '[failed]']);
}
$pspReference = (string) $item['pspReference'];
// Idempotent processing: skip items already handled in a previous delivery attempt
if ($this->processedRegistry->wasProcessed($pspReference, (string) $item['eventCode'])) {
continue;
}
$this->processedRegistry->markProcessed($pspReference, (string) $item['eventCode']);
}
return $result->setData(['notificationResponse' => '[accepted]']);
}
public function createCsrfValidationException(RequestInterface $request): ?InvalidRequestException
{
return null;
}
public function validateForCsrf(RequestInterface $request): bool
{
return true;
}
}
6. Magento Payment-Method-Facade: eigene Gateway-Command-Klassen für beide Provider
Magento kapselt jede Zahlungsmethode über die Payment-Method-Facade, umgesetzt als Magento\Payment\Model\Method\Adapter, kombiniert mit einem CommandPool aus dem Gateway-Command-Pattern. Für eine saubere PayPal- und Adyen-Integration bedeutet das: zwei vollständig getrennte CommandPools, jeweils mit eigenen Capture- und Refund-Command-Klassen, die über di.xml als virtualType verdrahtet werden, statt eine einzige generische Command-Klasse mit internen if-Verzweigungen pro Provider zu bauen.
Diese Trennung ist kein Selbstzweck: Die PayPal-Capture-Command muss die im Frontend erzeugte PayPal-Order-ID kennen und gegen die Orders API aufrufen, während die Adyen-Capture-Command gegen die Payments API mit einer originalReference auf die vorherige Autorisierung arbeitet. Beide Command-Klassen implementieren dasselbe CommandInterface, ihre interne Logik ist aber grundverschieden, was für die Magento Payment-Method-Facade unproblematisch ist, solange die Verdrahtung in di.xml sauber pro Provider getrennt bleibt.
Das folgende di.xml-Snippet zeigt, wie zwei unabhängige CommandPools und zwei unabhängige Facade-virtualTypes für PayPal und Adyen nebeneinander existieren, ohne dass sich Command-Klassen oder Konfiguration überschneiden.
<?xml version="1.0"?>
<!-- File: app/code/Mironsoft/Payment/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<!-- PayPal gateway command pool: one command class per operation -->
<virtualType name="MironsoftPaypalCommandPool" type="Magento\Payment\Gateway\Command\CommandPool">
<arguments>
<argument name="commands" xsi:type="array">
<item name="capture" xsi:type="string">Mironsoft\Payment\Gateway\Paypal\CaptureCommand</item>
<item name="refund" xsi:type="string">Mironsoft\Payment\Gateway\Paypal\RefundCommand</item>
</argument>
</arguments>
</virtualType>
<!-- Adyen gateway command pool: separate implementation, same Command Pattern -->
<virtualType name="MironsoftAdyenCommandPool" type="Magento\Payment\Gateway\Command\CommandPool">
<arguments>
<argument name="commands" xsi:type="array">
<item name="capture" xsi:type="string">Mironsoft\Payment\Gateway\Adyen\CaptureCommand</item>
<item name="refund" xsi:type="string">Mironsoft\Payment\Gateway\Adyen\RefundCommand</item>
</argument>
</arguments>
</virtualType>
<virtualType name="MironsoftPaypalFacade" type="Magento\Payment\Model\Method\Adapter">
<arguments>
<argument name="code" xsi:type="const">Mironsoft\Payment\Model\Paypal\ConfigProvider::CODE</argument>
<argument name="commandPool" xsi:type="object">MironsoftPaypalCommandPool</argument>
</arguments>
</virtualType>
<virtualType name="MironsoftAdyenFacade" type="Magento\Payment\Model\Method\Adapter">
<arguments>
<argument name="code" xsi:type="const">Mironsoft\Payment\Model\Adyen\ConfigProvider::CODE</argument>
<argument name="commandPool" xsi:type="object">MironsoftAdyenCommandPool</argument>
</arguments>
</virtualType>
</config>
7. Unterschiede in der Order-State-Machine: Pending, Authorized und Captured bei PayPal vs. Adyen
Beide Provider durchlaufen konzeptionell ähnliche Zustände, benennen und melden sie aber unterschiedlich. Bei PayPal liefert die Orders API einen Order-Status wie CREATED, APPROVED oder COMPLETED zurück, direkt als Antwort auf einen synchronen API-Aufruf. Bei Adyen hingegen kommt der State-Übergang fast ausschließlich asynchron über Notification-EventCodes wie AUTHORISATION, CAPTURE oder REFUND, jeweils mit einem success-Flag, das echten Erfolg von einer nur angenommenen Anfrage unterscheidet.
Für die Magento Order-State-Machine bedeutet das: Eine PayPal-Integration kann den Order-Status oft schon direkt nach dem synchronen API-Aufruf setzen, während eine Adyen-Integration zwingend auf die asynchrone Notification warten muss, bevor eine Order verlässlich als authorized oder captured markiert werden darf. Wer diesen Unterschied ignoriert und bei Adyen den Order-Status bereits nach dem Redirect setzt, riskiert Bestellungen, die als bezahlt gelten, obwohl die eigentliche Autorisierung serverseitig noch aussteht oder sogar fehlgeschlagen ist.
Die folgende Tabelle stellt die wichtigsten Architektur-Aspekte einer PayPal- und Adyen-Integration direkt gegenüber.
| Architektur-Aspekt | PayPal | Adyen |
|---|---|---|
| Order-Erstellung | Clientseitig über Smart Payment Buttons, JS SDK ruft createOrder auf, Order-ID entsteht im Browser-Kontext | Serverseitig über die Sessions API, Magento erzeugt die Payment Session vor dem Rendern des Drop-in |
| Checkout-Komponente | Smart Payment Buttons, JS SDK mit Popup- oder Redirect-Flow | Drop-in Component, eingebettetes Web Component ohne notwendigen Redirect |
| Webhook-Mechanismus | PayPal Webhooks, event-basiert, ein Event pro Request | Adyen Notifications, notificationItems Array, mehrere Items pro Request möglich |
| Signaturprüfung | Transmission-Signatur über PayPal-Zertifikat, Paypal-Transmission-Sig Header, Cert-URL-Abruf | HMAC-SHA256 über hmacSignature in additionalData, geteilter symmetrischer Key |
| State-Mapping | Order-Status CREATED, APPROVED, COMPLETED aus synchroner API-Antwort | Notification-EventCodes AUTHORISATION, CAPTURE, REFUND mit success-Flag |
Das State-Mapping in der Tabelle macht deutlich, warum ein einheitliches internes Status-Enum in der eigenen Magento-Erweiterung sinnvoll ist: Es übersetzt sowohl PayPals synchrone Order-Status als auch Adyens asynchrone EventCodes auf dieselbe interne Zustandsmenge, sodass nachgelagerte Prozesse wie Rechnungsstellung oder Versand nicht direkt von der jeweiligen Provider-Terminologie abhängen.
8. Refunds und Captures: API-Unterschiede bei Teilerstattung und Teilkapturierung
Eine Teilkapturierung, also das Erfassen eines geringeren Betrags als der ursprünglich autorisierte, funktioniert bei PayPal über den Capture-Endpunkt der Orders API mit dem Feld final_capture. Steht final_capture auf false, bleibt die Autorisierung für weitere Teilkapturierungen offen, bis entweder der volle Betrag erfasst oder die Autorisierung explizit abgeschlossen wird. Die folgende Command-Klasse zeigt, wie eine PayPal-Integration diese Unterscheidung zwischen voller und Teilkapturierung serverseitig abbildet.
<?php
declare(strict_types=1);
namespace Mironsoft\Payment\Gateway\Paypal;
use Magento\Payment\Gateway\CommandInterface;
use Magento\Payment\Gateway\Data\PaymentDataObjectInterface;
use Mironsoft\Payment\Model\Paypal\OrdersApiClient;
/**
* Captures a previously authorized PayPal order, either fully or partially.
*/
final class CaptureCommand implements CommandInterface
{
public function __construct(
private readonly OrdersApiClient $ordersApiClient,
) {
}
/**
* Executes the capture call against the PayPal Orders API v2 endpoint.
*
* @param array<string, mixed> $commandSubject
* @return void
*/
public function execute(array $commandSubject): void
{
/** @var PaymentDataObjectInterface $paymentDataObject */
$paymentDataObject = $commandSubject['payment'];
$payment = $paymentDataObject->getPayment();
$amount = (float) ($commandSubject['amount'] ?? 0.0);
$paypalOrderId = (string) $payment->getAdditionalInformation('paypal_order_id');
$isPartialCapture = $amount < (float) $payment->getOrder()->getGrandTotal();
// Partial captures require final_capture=false so the authorization stays open
$captureResponse = $this->ordersApiClient->captureOrder(
orderId: $paypalOrderId,
amount: $amount,
currencyCode: (string) $payment->getOrder()->getOrderCurrencyCode(),
isFinalCapture: !$isPartialCapture,
);
$payment->setTransactionId((string) $captureResponse['id']);
$payment->setIsTransactionClosed($captureResponse['status'] === 'COMPLETED' && !$isPartialCapture);
}
}
Adyen behandelt Teilkapturierungen und Teilerstattungen strukturell ähnlich, technisch aber über andere Endpunkte: Ein Capture-Aufruf gegen die Payments API referenziert die ursprüngliche pspReference, ein Refund-Aufruf ebenso. Mehrere Teilerstattungen gegen dieselbe pspReference sind bei Adyen ausdrücklich vorgesehen, solange die Summe aller Erstattungen den ursprünglich erfassten Betrag nicht übersteigt, was Adyen serverseitig validiert und bei Überschreitung mit einem Fehler quittiert.
Der praktische Unterschied für eine PayPal- und Adyen-Integration liegt vor allem in der Fehlerbehandlung: PayPal meldet einen abgelehnten Capture-Versuch synchron als HTTP-Fehler mit strukturiertem Fehlercode, während Adyen einen Refund- oder Capture-Request zunächst synchron nur entgegennimmt und das tatsächliche Ergebnis, etwa REFUND_FAILED, erst über die asynchrone Notification meldet. Wer beide Flows in derselben Refund-Logik behandelt, muss diesen Unterschied in der Fehlerrückmeldung explizit berücksichtigen.
9. Sicherheits- und PCI-Aspekte: Tokenisierung und Unterschiede bei 3D Secure 2
Beide Provider reduzieren den PCI-DSS-Scope von Magento drastisch, indem Kartendaten nie den eigenen Server durchlaufen. Bei PayPal übernimmt entweder der gehostete Checkout-Flow oder, bei aktivierten Advanced Card Fields, ein iframe-basiertes Feld-Set die Kartendatenerfassung. Bei Adyen erledigt das Drop-in Component dieselbe Aufgabe über gekapselte, isolierte Eingabefelder, sodass in beiden Fällen kein Klartext einer Kartennummer den eigenen Magento-Server erreicht.
Für wiederkehrende Zahlungen setzt PayPal auf Vault-Tokens, die über die REST API mit dem Kunden verknüpft und bei Folgebestellungen referenziert werden, während Adyen ein eigenes Tokenisierungs-Schema mit recurringProcessingModel verwendet, das zwischen einmaligen und wiederkehrenden Belastungen unterscheidet. Eine sorgfältige PayPal- und Adyen-Integration muss diese unterschiedlichen Token-Formate strikt getrennt speichern, da ein PayPal-Vault-Token bei Adyen wertlos ist und umgekehrt. Genau hier zahlt sich eine sauber getrennte Zahlungsintegration mit eigenen Gateway-Command-Klassen aus.
Bei 3D Secure 2 unterscheiden sich beide Anbieter im Ablauf der Challenge: PayPal delegiert die 3DS-Authentifizierung weitgehend an den zugrunde liegenden Kartenzahlungs-Flow und meldet das Ergebnis über liability_shift im API-Response. Adyen steuert 3D Secure 2 direkt über das Drop-in Component, inklusive nativer Challenge-Darstellung ohne vollständigen Seiten-Redirect, was in der Praxis zu spürbar weniger Kaufabbrüchen führt als klassische Redirect-basierte 3DS-Flows. Für jede moderne PayPal- und Adyen-Integration ist die korrekte Behandlung von 3D Secure 2 kein optionales Extra, sondern durch PSD2 und SCA-Vorgaben in Europa praktisch verpflichtend.
10. Zusammenfassung
Eine PayPal- und Adyen-Integration in Magento 2 ist technisch anspruchsvoller als der Austausch zweier gleichwertiger Zahlungsmethoden. PayPal setzt auf clientseitige Order-Erstellung über Smart Payment Buttons und zertifikatsbasierte Webhook-Signaturen, Adyen auf serverseitige Session-Erstellung über das Drop-in Component und HMAC-signierte Notification-Batches. Beide Architekturen erfordern eigene Gateway-Command-Klassen, eigene Webhook-Controller und eine saubere Abbildung auf eine gemeinsame interne Order-State-Machine.
Wer diese Unterschiede von Anfang an respektiert, statt einen generischen Adapter für beide Provider zu bauen, vermeidet die typischen Fehlerquellen bei Teilerstattungen, doppelten Webhook-Zustellungen und inkonsistenten Order-Status. Eine sauber getrennte PayPal-Integration und Adyen-Integration, jeweils mit eigener Command-Klasse, eigener Signaturprüfung und eigenem State-Mapping, ist die Grundlage für eine stabile, PCI-konforme Zahlungsintegration, die auch bei hohem Transaktionsvolumen zuverlässig funktioniert.
PayPal- und Adyen-Integration in Magento 2: Das Wichtigste auf einen Blick
Architektur
PayPal erstellt Orders clientseitig über Smart Payment Buttons, Adyen erstellt Payment Sessions serverseitig über die Sessions API vor dem Drop-in.
Webhooks
PayPal liefert ein zertifikatsignertes Event pro Request, Adyen liefert HMAC-signierte notificationItems, teils mehrere pro Request, idempotent zu verarbeiten.
Payment-Facade
Zwei getrennte CommandPools und virtualTypes in di.xml, eigene Capture- und Refund-Commands pro Provider statt eines generischen Adapters.
Sicherheit
Getrennte Tokenisierungs-Schemata, unterschiedliche 3D-Secure-2-Abläufe und strikte Signaturprüfung sind für eine PCI-konforme Zahlungsintegration Pflicht.
11. FAQ: PayPal- und Adyen-Integration in Magento 2
1Was ist der wichtigste Architektur-Unterschied zwischen PayPal- und Adyen-Integration?
2Wie erstellt PayPal eine Order im Vergleich zu Adyen?
3Was unterscheidet PayPal Webhooks von PayPal IPN?
4Wie funktioniert die HMAC-Signaturprüfung bei Adyen?
5Warum enthält ein Adyen-Request mehrere Events?
6Wie sieht die Payment-Method-Facade für beide Provider aus?
7Welche Order-Status durchläuft eine PayPal-Zahlung?
8Wie unterscheiden sich Teilerstattungen bei PayPal und Adyen?
9Was bedeutet idempotente Verarbeitung bei Webhooks?
10Welche Rolle spielt 3D Secure 2?
Mironsoft
Zahlungsanbieter-Integration, Webhook-Sicherheit und Custom-Payment-Gateways für Magento 2
Wie robust ist eure PayPal- und Adyen-Integration wirklich?
Wir prüfen eure bestehende Zahlungsintegration auf Webhook-Sicherheit, State-Machine-Konsistenz und PCI-relevante Aspekte, und bauen bei Bedarf eigene Gateway-Command-Klassen für PayPal, Adyen oder einen weiteren Provider.
Zahlungsanbieter-Integration
Saubere PayPal- und Adyen-Integration mit eigenen Gateway-Command-Klassen statt generischer Adapter-Logik
Webhook-Sicherheitsaudit
Prüfung der Signaturverfahren, Idempotenz und Fehlerbehandlung eurer bestehenden Webhook-Controller
Custom-Payment-Gateway-Entwicklung
Eigene Payment-Method-Facade und Command-Pattern-Implementierung für Anbieter außerhalb der Standard-Extensions