netshell/paypal
Laravel package for PayPal integration that provides a fluent wrapper around the PayPal API, with support for Express Checkout, recurring payments, transactions, and webhook/IPN handling to simplify common payment flows.
composer require netshell/paypal
This is a Laravel 5-era package, so on modern Laravel you will likely need to register the provider and facade manually in config/app.php (no auto-discovery). If it fails to install on your Laravel version, that is the first signal to stop and reconsider.
'providers' => [
Netshell\Paypal\PaypalServiceProvider::class,
],
'aliases' => [
'Paypal' => Netshell\Paypal\Facades\Paypal::class,
],
Publish and fill the config (credentials come from your PayPal account's API signature, under Profile → API Access):
php artisan vendor:publish --provider="Netshell\Paypal\PaypalServiceProvider"
// config/paypal.php
return [
'username' => env('PAYPAL_USERNAME'),
'password' => env('PAYPAL_PASSWORD'),
'signature' => env('PAYPAL_SIGNATURE'),
'sandbox' => env('PAYPAL_SANDBOX', true),
];
Where to look first: vendor/netshell/paypal/src/. The package is a thin NVP wrapper — one class behind the facade. Read it end to end (it is short) to see exactly which Express Checkout calls are exposed and how the sandbox/live endpoint is chosen. Do not assume the README covers everything; the source is the contract.
First use case: a single-item checkout. Three routes and one controller:
GET /checkout → build SetExpressCheckout params, call Paypal::setExpressCheckout($params), redirect the user to PayPal using the returned token/URL.GET /paypal/return (your RETURNURL) → read token and PayerID from the query string, call Paypal::getExpressCheckoutDetails($token) to confirm the payer.Paypal::doExpressCheckoutPayment($token, $payerId, $params) and persist the result.Start in sandbox, with a sandbox buyer account, before touching live credentials.
Keep a single source of truth for the amount. The amount you send to SetExpressCheckout and the amount you send to DoExpressCheckoutPayment must match. Compute the cart total once, persist it against the token, and read it back on return. Drift here produces 10413 / 10414 errors that are annoying to diagnose.
Persist the token before redirecting. Create a payments row keyed on the PayPal token (unique index) with status, amount, currency, and your order reference. The return URL is a fresh request with no session continuity guarantee — everything you need must be in the DB or signed into the URL.
Wrap the facade in your own gateway. The facade is a singleton proxy; using it directly in controllers couples your app to a deprecated API. Define an interface (initiate, confirm, capture) and implement it with this package. When you migrate to PayPal Orders v2 you swap the implementation, not the call sites. This also gives you a seam to bind a fake in tests.
A typical controller action:
use Netshell\Paypal\Facades\Paypal;
public function start(Request $request)
{
$order = Order::createFromCart(session('cart'));
$params = [
'RETURNURL' => route('paypal.return'),
'CANCELURL' => route('paypal.cancel'),
'PAYMENTREQUEST_0_PAYMENTACTION' => 'Sale',
'PAYMENTREQUEST_0_AMT' => number_format($order->total, 2, '.', ''),
'PAYMENTREQUEST_0_CURRENCYCODE' => 'USD',
'PAYMENTREQUEST_0_ITEMAMT' => number_format($order->subtotal, 2, '.', ''),
'L_PAYMENTREQUEST_0_NAME0' => $order->description,
'L_PAYMENTREQUEST_0_QTY0' => 1,
'L_PAYMENTREQUEST_0_AMT0' => number_format($order->subtotal, 2, '.', ''),
];
$response = Paypal::setExpressCheckout($params);
abort_unless(($response['ACK'] ?? '') === 'Success', 502, 'PayPal init failed');
$order->update(['paypal_token' => $response['TOKEN']]);
return redirect(Paypal::getUrl($response['TOKEN'])); // or build the redirect from the token + sandbox flag
}
The exact redirect helper varies by version — confirm in the vendor source whether you get a full URL back or need to construct https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=... yourself.
Return handler: validate token matches your row, call getExpressCheckoutDetails for the payer name/email, then doExpressCheckoutPayment. Record PAYMENTINFO_0_TRANSACTIONID immediately — that is your receipt.
Guard against double-submission. Users refresh return URLs, browsers retry, and webhooks (none ship with this package) may or may not fire. Make the return handler idempotent: if the order is already paid, redirect to the success page without calling PayPal again.
ACK. The wrapper returns the raw parsed NVP response array. Success and SuccessWithWarning are OK; anything else has L_ERRORCODE0 / L_LONGMESSAGE0. A surprising number of "PayPal is broken" reports are a 10417 (payer cannot pay) or 10413 (amount mismatch) being ignored.number_format($n, 2, '.', ''). Floating-point artifacts in AMT cause silent rejections. Line-item amounts plus ITEMAMT plus shipping/tax must reconcile to AMT or PayPal rejects the call wholesale.php artisan config:clear after any .env change. On a cached deployment, a stale signature produces opaque authentication failures.CANCELURL fires with a token but no payer; mark the order abandoned, do not treat it as an error.How can I help you explore Laravel packages today?