Payment Webhook Architecture
Payment Webhook Flow
Payment Gateway Magento 2 Store
│ │
1. Payment event occurs │
2. Send webhook │
├────────────────────────►│
│ 3. Verify signature │
│ 4. Parse event │
│ 5. Update order │
│ 6. Return 200 OK │
│◄────────────────────────┤
│ │
Webhook Controller
namespace Vendor\Payment\Controller\Webhook;
use Magento\Framework\App\Action\Action;
class StripeWebhook extends Action
{
private WebhookVerifierInterface $verifier;
private WebhookDispatcherInterface $dispatcher;
private JsonFactory $jsonFactory;
public function execute()
{
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
// Verify signature
if (!$this->verifier->verify($payload, $signature)) {
return $this->createResponse(401, 'Invalid signature');
}
$event = json_decode($payload, true);
// Dispatch to handler
$this->dispatcher->dispatch(
$event['type'],
$event['data']['object'] ?? [],
$event['id']
);
return $this->createResponse(200, 'OK');
}
}
Webhook Event Handlers
Event Handler Registry
namespace Vendor\Payment\Webhook\Handler;
interface WebhookHandlerInterface
{
public function handle(array $data, string $eventId): void;
public function getEventType(): string;
}
class PaymentSucceededHandler implements WebhookHandlerInterface
{
private OrderRepositoryInterface $orderRepo;
private TransactionRepositoryInterface $transactionRepo;
public function handle(array $data, string $eventId): void
{
$order = $this->orderRepo->getByIncrementId($data['metadata']['order_id']);
// Create transaction
$transaction = new TransactionData([
'order_id' => $order->getId(),
'transaction_id' => $data['payment_intent'],
'type' => 'capture',
'amount' => $data['amount'] / 100,
'status' => 'approved',
]);
$this->transactionRepo->save($transaction);
// Update order status
$order->setState(Order::STATE_PROCESSING);
$order->addCommentToStatusHistory('Payment received via webhook');
$this->orderRepo->save($order);
}
public function getEventType(): string
{
return 'payment_intent.succeeded';
}
}
class PaymentFailedHandler implements WebhookHandlerInterface
{
public function handle(array $data, string $eventId): void
{
$order = $this->orderRepo->getByIncrementId($data['metadata']['order_id']);
$order->setState(Order::STATE_CANCELED);
$order->addCommentToStatusHistory(
'Payment failed: ' . ($data['last_payment_error']['message'] ?? 'Unknown error')
);
$this->orderRepo->save($order);
}
public function getEventType(): string
{
return 'payment_intent.payment_failed';
}
}
Handler Dispatcher
namespace Vendor\Payment\Webhook\Handler;
class WebhookDispatcher implements WebhookDispatcherInterface
{
private array $handlers;
private IdempotencyCheckerInterface $idempotency;
private LoggerInterface $logger;
public function dispatch(string $eventType, array $data, string $eventId): void
{
// Check idempotency
if ($this->idempotency->isProcessed($eventId)) {
$this->logger->info('Duplicate webhook ignored', ['event_id' => $eventId]);
return;
}
$handler = $this->handlers[$eventType] ?? null;
if (!$handler) {
$this->logger->warning('No handler for event type', ['type' => $eventType]);
return;
}
try {
$handler->handle($data, $eventId);
$this->idempotency->markProcessed($eventId);
} catch (\Exception $e) {
$this->logger->error('Webhook handler failed', [
'event_type' => $eventType,
'event_id' => $eventId,
'error' => $e->getMessage(),
]);
throw $e;
}
}
}
Notification Verification
PayPal IPN Verification
namespace Vendor\Payment\Webhook\Verify;
class PayPalIpnVerifier implements NotificationVerifierInterface
{
private HttpClientInterface $httpClient;
public function verify(array $ipnData): bool
{
// Step 1: Build verification request
$verificationData = 'cmd=_notify-validate';
foreach ($ipnData as $key => $value) {
$verificationData .= '&' . $key . '=' . urlencode($value);
}
// Step 2: Send to PayPal for verification
$response = $this->httpClient->post(
'https://www.paypal.com/cgi-bin/webscr',
$verificationData,
['Content-Type' => 'application/x-www-form-urlencoded']
);
// Step 3: Check response
return trim($response->getBody()) === 'VERIFIED';
}
}
Webhook Signature Verification
namespace Vendor\Payment\Webhook\Verify;
class StripeSignatureVerifier implements NotificationVerifierInterface
{
private string $secret;
public function verify(string $payload, string $signatureHeader): bool
{
$elements = [];
foreach (explode(',', $signatureHeader) as $pair) {
[$key, $value] = explode('=', $pair, 2);
$elements[$key] = $value;
}
$timestamp = $elements['t'] ?? '';
$expectedSig = $elements['v1'] ?? '';
// Reject old payloads (replay protection)
$age = time() - (int) $timestamp;
if ($age > 300) {
return false;
}
$signedPayload = $timestamp . '.' . $payload;
$computedSig = hash_hmac('sha256', $signedPayload, $this->secret);
return hash_equals($expectedSig, $computedSig);
}
}
Async Payment Updates
Async Status Sync
namespace Vendor\Payment\Webhook\Sync;
class PaymentStatusSync
{
private QueueInterface $queue;
public function scheduleSync(
string $orderId,
string $status,
array $data
): void {
$this->queue->addMessage(new PaymentStatusMessage(
$orderId,
$status,
$data
));
}
}
namespace Vendor\Payment\Webhook\Sync\Handler;
class PaymentStatusHandler
{
public function process(PaymentStatusMessage $message): void
{
$order = $this->orderRepo->getByIncrementId($message->getOrderId());
match ($message->getStatus()) {
'succeeded' => $this->handleSucceeded($order, $message->getData()),
'failed' => $this->handleFailed($order, $message->getData()),
'refunded' => $this->handleRefunded($order, $message->getData()),
'disputed' => $this->handleDisputed($order, $message->getData()),
};
}
}
Webhook Log
namespace Vendor\Payment\Webhook\Log;
class WebhookLogger
{
private WebhookLogRepositoryInterface $logRepo;
public function log(
string $gateway,
string $eventType,
array $payload,
string $status
): void {
$log = new WebhookLogData([
'gateway' => $gateway,
'event_type' => $eventType,
'payload' => json_encode($payload),
'status' => $status,
'received_at' => new \DateTime(),
]);
$this->logRepo->save($log);
}
}
Quiz
1. Why verify payment webhook signatures?
2. What is idempotency in webhooks?
3. How does PayPal IPN verification work?
Flashcards
Question
What is payment webhook idempotency?
Click to reveal answer
Answer
Processing the same event only once, even if received multiple times
Question
How to verify Stripe webhooks?
Click to reveal answer
Answer
HMAC-SHA256 signature verification with timestamp
Question
How does PayPal IPN verify?
Click to reveal answer
Answer
Post data back to PayPal, they return VERIFIED/INVALID
Question
What does a webhook handler do?
Click to reveal answer
Answer
Processes payment events and updates order status
Revision Notes
Key Takeaways
- 1. Payment webhooks handle async status updates from gateways
- 2. Signature verification prevents spoofed webhook requests
- 3. Idempotency prevents duplicate processing of events
- 4. Event handlers map gateway events to order operations
- 5. Webhook logs provide audit trail for payment events
Interview Tips
- • Explain how webhook signature verification works
- • Discuss idempotency implementation for payment events
- • Describe handling async payment status updates
- • Talk about PayPal IPN vs Stripe webhooks
Cheat Sheet
Payment Webhooks:
Gateway → verify → dispatch → handler → order update
Verification:
Stripe: HMAC-SHA256 signature
PayPal: IPN verification post
Events:
payment_intent.succeeded → complete order
payment_intent.payment_failed → cancel order
charge.refunded → process refund
Idempotency:
event_id → processed status
Prevents duplicate processing