Merx

PayPal payment gateway

Learn how to integrate PayPal as a payment service provider.

PayPal is a redirect gateway. Merx creates an order at PayPal, sends the customer there to approve it, and captures the money when they come back. None of that happens on your site, so unlike Stripe Elements there’s no JavaScript to load and no payment form to build.

Set up the client ID and secret as described in Stripe/PayPal secrets. Merx uses the sandbox keys while production is false and the live ones afterwards.

Send the order to PayPal

If you followed the validate checkout guide, the only thing to change is the hidden payment gateway field. That’s it.

/site/templates/checkout.php
<form id="checkout-form" action="<?= url('api/shop/checkout') ?>" method="post">
  <!-- other fields -->
  <input type="hidden" name="payment_gateway" value="paypal">
  <button>Submit order</button>
</form>

Merx creates the order at PayPal, redirects the customer there to approve it, and captures the payment when PayPal sends them back to /api/shop/success. Then the order page is written, the cart is emptied and the customer lands on their order.

Test with a sandbox account

Create a REST app in the PayPal Developer Dashboard for the sandbox keys, and pay with one of the sandbox personal accounts under Testing Tools → Sandbox Accounts.

A sandbox account only works against the sandbox API, and a live account only against the live one. If a payment fails with error.merx.paypalError right after you go live, check that the paypal.live.* keys are really live keys.

When the payment does not complete

The customer cancels at PayPal. Merx raises error.merx.paymentCanceled and sends them back to your checkout page with the cart still filled.

PayPal declines the capture. Merx raises error.merx.paypalError and no order page is written.

The capture stays pending. With some funding sources PayPal reports the order as COMPLETED but leaves the capture in PENDING. Merx writes the order page but does not mark it as paid.

There is no PayPal webhook. A pending capture that settles later is not reported back to your shop the way a delayed Stripe payment is, so those orders need a look in the PayPal dashboard and a manual change of the order.

Merx also refuses a PayPal order that has been captured once already, so a customer who reloads the return URL cannot pay twice or complete somebody else’s order.

Change what the customer sees at PayPal

Merx sends your site title as the brand name, PAY_NOW so the button says Pay Now, and NO_SHIPPING, because the address was already collected in your checkout. Override any of them with paypal.applicationContext, which is merged over those defaults.

/site/config/config.php
<?php

return [
  'wagnerwagner.merx.paypal.applicationContext' => [
    'brand_name' => 'Nice New Shoes',
    'locale' => 'de-DE',
  ],
];

Leave return_url and cancel_url alone — Merx sets both to the return URL that carries the session token, and a payment that comes back anywhere else cannot be completed.

Show the cart at PayPal

By default PayPal shows one line with your site title and the cart total. $cart->payPalPurchaseUnits() builds the same order with every item listed instead, which is what the paypal.purchaseUnits option expects.

/site/config/config.php
<?php

return [
  'wagnerwagner.merx.paypal.purchaseUnits' => fn (): array => site()->cart()->payPalPurchaseUnits(),
];

What the option returns replaces Merx’ own purchase unit rather than adding to it — PayPal charges the sum of all units, so returning a second complete one would have the customer approve the total twice. Merx only adds the custom_id that binds the order to the payment.

PayPal adds the items up itself and rejects an order whose numbers do not add up. If you build the units by hand, the item total plus the discount has to match the amount exactly.