payum/paypal-pro-checkout-nvp
Payum gateway for PayPal Payments Pro using the legacy NVP (Name-Value Pair) API, enabling direct credit card payments and recurring profiles through PayPal Pro Checkout. Integrates with Payum's payment abstraction for Laravel and PHP applications.
What it is. A Payum gateway that talks to PayPal's legacy NVP (Name-Value Pair) API for PayPal Payments Pro — i.e. a card-not-present form where you POST the card fields from your own checkout page (DoDirectPayment semantics), not a PayPal-hosted redirect. If you actually want the "redirect to PayPal, come back with a token" flow, you want payum/paypal-express-checkout-nvp instead.
Viability check before you commit. Last release 2016, 3 stars, 0 dependents, and it pins old payum/core constraints. Run these first:
composer show --all payum/paypal-pro-checkout-nvp # see requires + available versions
composer why-not payum/paypal-pro-checkout-nvp ^1.0 # what's blocking install
If you're on Payum 2.x or PHP 8+, expect to either pin an older Payum or vendor the Api class into your own gateway. Decide that before writing checkout code.
Install and configure.
composer require payum/paypal-pro-checkout-nvp
Plain Payum:
$payum = (new \Payum\Core\PayumBuilder())
->addDefaultStorages()
->addGateway('paypal_pro', [
'factory' => 'paypal_pro_checkout_nvp',
'username' => getenv('PAYPAL_PRO_USERNAME'),
'password' => getenv('PAYPAL_PRO_PASSWORD'),
'signature' => getenv('PAYPAL_PRO_SIGNATURE'),
'sandbox' => true,
])
->getPayum();
$gateway = $payum->getGateway('paypal_pro');
Symfony/PayumBundle:
payum:
gateways:
paypal_pro:
factory: paypal_pro_checkout_nvp
username: '%env(PAYPAL_PRO_USERNAME)%'
password: '%env(PAYPAL_PRO_PASSWORD)%'
signature: '%env(PAYPAL_PRO_SIGNATURE)%'
sandbox: true
Where to look first, in this order:
src/Payum/Paypal/ProCheckout/Nvp/PaypalProCheckoutNvpGatewayFactory.php — confirms the factory name paypal_pro_checkout_nvp and which config keys (username, password, signature, sandbox) actually exist.src/Payum/Paypal/ProCheckout/Nvp/Api.php — the whole NVP surface in one file. This tells you definitively whether you get doDirectPayment only, or also doCapture, refundTransaction, etc.src/Payum/Paypal/ProCheckout/Nvp/Action/* — how Capture/Authorize requests get translated into NVP parameters and how ACK/errors are surfaced.Tests/ApiTest.php — the fastest documentation you'll find: it shows the exact NVP parameter names the class emits.First use case — take a card payment. Fill the payment model's details with the Pro billing/card fields, then Capture:
use Payum\Core\Request\Capture;
use Payum\Core\Model\Payment;
$payment = new Payment();
$payment->setNumber('ORDER-1042');
$payment->setTotalAmount(1999); // minor units
$payment->setCurrencyCode('USD');
$payment->setDetails([
'FIRSTNAME' => 'John',
'LASTNAME' => 'Doe',
'STREET' => '1 Main St',
'CITY' => 'San Jose',
'STATE' => 'CA',
'ZIP' => '95131',
'COUNTRYCODE' => 'US',
'CREDITCARDTYPE' => 'Visa',
'ACCT' => '4111111111111111',
'EXPDATE' => '122030', // MMYYYY, no slash
'CVV2' => '123',
'IPADDRESS' => $request->getClientIp(),
]);
$gateway->execute(new Capture($payment));
// Inspect $payment->getDetails() for ACK, TRANSACTIONID, L_ERRORCODE0...
Treat details as the NVP envelope. This gateway is thin: whatever you put in details is merged into the request, and whatever PayPal returns is merged back. Use UPPERCASE PayPal parameter names consistently and keep your own application data (order id, customer id) under lowercase custom keys or in a separate column so you never collide with PayPal's namespace.
Direct-capture vs. authorize-then-capture. DoDirectPayment supports PAYMENTACTION=Authorization followed by DoCapture, which matters if you ship physical goods and want to capture on dispatch. Check whether the shipped actions map Capture to Sale unconditionally; if so, either accept immediate settlement or write a small action that sets PAYMENTACTION explicitly. Don't assume two-step works until you've read the action.
Never persist raw card data. The Payment model's details array gets serialized into your token/payment storage. Strip or mask ACCT, CVV2, EXPDATE after capture, or wrap them in Payum's sensitive-value wrapper (Payum\Core\Security\SensitiveValue if your Payum version ships it) so they're masked in dumps and refused by the serializer. Verify with a SELECT on the stored row before going anywhere near production.
Store the transaction identity, not just a boolean. Persist TRANSACTIONID, CORRELATIONID, ACK, AMT, CURRENCYCODE, and TIMESTAMP on your order. When a support ticket arrives three weeks later, TRANSACTIONID is the only handle PayPal support will accept. CORRELATIONID is what PayPal engineering uses to trace a request — log it on every failure.
Testing. Two layers:
Api (override the api instance in the gateway config, or construct the gateway manually and setApi()), assert your code emits the right details and handles ACK => Failure without exploding.sandbox => true against a sandbox seller account that actually has Payments Pro enabled — a vanilla sandbox account will reject you with error 10501.Symfony integration. Keep the gateway call inside a service, not a controller, because the Capture request needs the client IP (required for IPADDRESS) and you don't want controllers reaching into the HTTP foundation for that. Return an order status object; let the controller render.
Reconciliation job. Because there's no webhook, poll. A nightly job that re-queries uncertain transactions (timeouts, ACK unknown) and matches on TRANSACTIONID is not optional — it is the only way this integration stays correct.
The package is effectively unmaintained. Last release 2016. The practical consequence is dependency hell, not broken code — the Api class is simple string-building and will keep working long after the package metadata rots. If composer won't resolve, the cheapest fix is usually to copy Api.php plus your one action into App\Payum and register your own gateway factory, rather than fighting version constraints.
You're in PCI scope. Card numbers transit your server. This is SAQ D territory, not SAQ A. If that's a problem organisationally, this package is the wrong choice — use a hosted-fields or REST-with-tokenisation integration where the PAN never touches your infrastructure.
IPADDRESS is mandatory. DoDirectPayment requires the buyer's IP; omit it and you get a generic "there's an error with this transaction" with no useful hint. In dev with no request context, pass a fixed placeholder rather than letting it be null.
Format traps. EXPDATE is MMYYYY with no separator (122030, not 12/2030, not 2030-12). AMT must be a dot-decimal string; Payum converts your minor-unit totalAmount for you, but if you set AMT in details yourself it will not be re-converted. Currency must be one the merchant account is provisioned for.
Error handling. PayPal returns ACK of Success, SuccessWithWarning, or Failure, plus indexed L_ERRORCODE0..n / L_SHORTMESSAGE0..n / L_LONGMESSAGE0..n. Catch the exception the Api throws, then re-read the raw response from details — the exception message alone usually drops the long message, which is where the real cause lives. Codes you'll meet: 10501 (merchant not configured for Pro), 10502 (card expired), 10507 (CVV2 mismatch), 10527 (invalid data — almost always a malformed field), 10417 (instruct customer to use another payment method), 15005 (processor declined).
No refund, void, recurring, or 3-D Secure support ships here. These are the extension points you'll actually need:
RefundTransaction / DoVoid through the same credentials, registered on your own gateway factory (populateGateway() → parent::populateGateway($gateway) then $gateway->addAction(...)).RETURNFMFDETAILS, but nothing in this package handles the ACS redirect. Budget real time for it, or scope it out explicitly.CreateRecurringPaymentsProfile is not wrapped; you'd be writing raw NVP.No idempotency key. Retrying a timed-out DoDirectPayment can double-charge. Decide on a policy now: on timeout, don't retry — mark the order pending_review and reconcile by querying TRANSACTIONID/searching the day's transactions before re-attempting.
Debug by dumping the NVP pair. When a request fails opaquely, log the outgoing parameter string with card fields masked and the raw response body. Nine out of ten failures are a typo'd parameter name or a missing required address field, and the full pair makes it obvious in seconds.
How can I help you explore Laravel packages today?