professionalweb/payment-laravel
A Laravel package that simplifies online payment integration through a unified API, configurable gateways, payment requests, verification, and callback handling for adding checkout flows to Laravel applications.
This package is a thin Laravel wrapper (~15 stars, last release October 2018) around professionalweb/payment, providing a Manager-style facade and driver implementations for PayOnline, Tinkoff, Yandex.Kassa, processing.kz, and upc.ua. It is a low-level, driver-based abstraction — not a full commerce solution — so expect to own the order model, routes, idempotency, and reconciliation logic yourself.
Minimal setup:
composer require professionalweb/payment-laravelconfig/app.php; 5.5+ auto-discovers) and, if you want the terse syntax, the Payment facade.php artisan vendor:publish --provider="ProfessionalWeb\Payment\PaymentServiceProvider" — you get a config/payment.php with a drivers block per gateway. Fill in merchant IDs, terminal keys, private/secret keys, and the success/fail/notification URLs.ProfessionalWeb\Payment\Drivers\* (or the underlying professionalweb/payment package — most of the real logic lives there, not in the Laravel repo). That source file is your best "documentation": the interface methods are what you'll be calling.First use case: the checkout redirect flow. Build an order, hand it to a driver to generate a payment form/URL, redirect the customer to the gateway, then handle the return (user-facing) and notification/callback (server-to-server) routes. The driver gives you the form/URL and lets you verify the callback — everything else (models, state machine, receipts) is on you.
Resolve a driver per gateway. The facade exposes a manager, so you typically write:
use ProfessionalWeb\Payment\Facades\Payment;
$driver = Payment::driver('tinkoff'); // or 'payonline', 'yandex_kassa', ...
$form = $driver->getPaymentForm($order); // or getPaymentUrl($order)
Wrap that call in your own PaymentGateway service/interface so controllers never depend on the string names — you'll thank yourself when a gateway is dropped or renamed.
Two-route pattern per gateway. Standard shape:
// routes/web.php
Route::post('/pay/{gateway}/notify', [PayController::class, 'notify'])->name('pay.notify');
Route::get('/pay/{gateway}/return', [PayController::class, 'return'])->name('pay.return');
notify — server-to-server callback. Verify signature via the driver, look up the order using the ID the driver exposes (via getOrderId($request) or the raw payload), confirm the amount matches what you recorded, then mark the order paid. Respond with whatever body the gateway expects (often a terse string, not JSON — check the driver).return — user-facing redirect after the customer finishes on the gateway. Never mark the order paid here; only show status. Real payment confirmation must come from notify.Idempotency is mandatory. The notify route gets retried by gateways on timeout, and by support engineers. Store the gateway transaction ID on the order with a unique index; if you see the same callback twice, return success without reprocessing. Keep a raw-payload log table (gateway, order ID, body, IP, timestamp) — it makes 3am debugging possible.
Config per environment. Keep separate credential blocks and gateway URLs for sandbox vs. production in config/payment.php, and select via APP_ENV. Never put merchant private keys in a .env file you share across the team — use per-developer .env with sandbox keys.
Amount handling. Most of these gateways (Tinkoff, PayOnline, Yandex.Kassa) expect amounts in minor units (kopecks) in some APIs and major units in others. Do not trust the driver to normalise; assert in your own Order class and write a test that confirms round-tripping (create → callback → compare).
Test harness. Because the drivers are thin HTTP wrappers, you can unit-test signature verification by feeding fixture payloads from each gateway's docs into ->verify($payload). Build fixtures for at least: successful payment, failed payment, refund notification, malformed signature. That is your regression safety net given the package is unmaintained.
Integration with queues. Do your notification processing inside a queued job — the callback route should only: log, verify, dispatch, return 200. If the gateway is slow to give up, you do not want a queued retry storm hitting a synchronous handler that calls out to your DB under load.
professionalweb/payment into your own code and depend on Guzzle directly. Treat the package as reference source, not a maintained dependency.SecurityKey-hashed concatenation; Tinkoff uses a SHA-256 of sorted fields + secret; Yandex.Kassa uses a separate notification protocol. Do not assume you can share verification code across drivers. Read each driver's verify() and write a test per driver.verify() return value is easy to misuse. Most drivers return a boolean, but some also expose getOrderId() / getOrderAmount() that must be called after verify(). Read the driver's method order and reproduce it exactly — reversing calls silently produces zero amounts or null order IDs.url()->route(...) not route(...) returns relative) and URL-safe. A trailing slash mismatch will break signature checks on some gateways.VerifyCsrfToken middleware will 419 your callbacks. Add the notify path to $except — but only the notify path; keep CSRF on the return path if it goes through web middleware.config/payment.php — each driver block typically has its own url/payment_url. Copy-pasting a production block into a sandbox config is a classic mistake.PaymentServiceProvider binds each driver) — no need to fork if you only need one extra provider.Gateway interface with createPayment(Order): string, verify(Request): VerifiedPayment, and put the package drivers behind it. When (not if) a driver becomes stale, swapping it out is a one-class change instead of a controller refactor.How can I help you explore Laravel packages today?