laravel-paynow maintained by beziworld
Laravel Paynow
Paynow (mBank) payment gateway for Laravel, API v3: payments, BLIK level 0, refunds, payment methods and HMAC-signed notifications. Verified against the Paynow sandbox.
Installation
composer require beziworld/laravel-paynow
php artisan vendor:publish --tag=paynow-config # optional
PAYNOW_API_KEY=your-api-key
PAYNOW_SIGNATURE_KEY=your-signature-key
PAYNOW_SANDBOX=true
PAYNOW_CONTINUE_URL=https://shop.example.com/payment/return
Set the notification URL of the shop in the Paynow panel to https://your-app.com/paynow/notification.
Payment flow
use Beziworld\Paynow\Data\PaymentRequest;
use Beziworld\Paynow\Exceptions\ApiException;
use Beziworld\Paynow\Facades\Paynow;
public function pay(Order $order): RedirectResponse
{
try {
$payment = Paynow::createPayment(new PaymentRequest(
amount: $order->total_in_grosze, // integer minor units, at least 100
externalId: $order->number,
description: "Order {$order->number}",
buyerEmail: $order->email,
buyer: ['firstName' => $order->first_name, 'lastName' => $order->last_name, 'locale' => 'pl-PL'],
));
} catch (ApiException $e) {
report($e);
return back()->withErrors('The payment could not be started.');
}
$order->update(['paynow_payment_id' => $payment->paymentId]);
return redirect()->away($payment->redirectUrl);
}
Every request carries an idempotency key. For payments it is derived from the external id and the amount, so a repeated call for the same order (e.g. after a timeout) returns the original payment instead of charging twice.
Notifications
use Beziworld\Paynow\Events\PaymentConfirmed;
use Beziworld\Paynow\Events\PaymentStatusChanged;
Event::listen(function (PaymentConfirmed $event) {
$order = Order::where('number', $event->notification->externalId)->firstOrFail();
if (! $order->isPaid()) {
$order->markAsPaid(paynowPaymentId: $event->notification->paymentId);
}
});
Event::listen(function (PaymentStatusChanged $event) {
// NEW, PENDING, CONFIRMED, REJECTED, ERROR, EXPIRED, ABANDONED
});
The signature is verified over the raw request body. Paynow may send notifications more than once and out of order, so keep listeners idempotent and never move an order back from a final status.
BLIK level 0 (code entered in your shop)
$payment = Paynow::createPayment(new PaymentRequest(
amount: 4999,
externalId: $order->number,
description: 'Subscription',
buyerEmail: $user->email,
paymentMethodId: Paynow::blikMethodId(4999), // requires white label to be enabled
authorizationCode: $request->input('blik_code'),
));
// No redirect: the buyer confirms in the banking app, the result arrives as a notification.
Errors such as AUTHORIZATION_CODE_INVALID or AUTHORIZATION_CODE_EXPIRED are available through
ApiException::hasErrorType().
Refunds, status and payment methods
use Beziworld\Paynow\Enums\RefundReason;
$refund = Paynow::refund($order->paynow_payment_id, 2500, RefundReason::ReturnOfGoods, "refund-{$return->id}");
Paynow::refundStatus($refund->refundId)->status; // RefundStatus::Successful
Paynow::paymentStatus($paymentId); // PaymentStatus::Confirmed
Paynow::paymentMethods(amount: 4999); // list<PaymentMethod> with logos for your own picker
Pass a stable idempotency key to refund() (e.g. the id of the return) to make retries safe.
Notes
continueUrlmust be a valid public ASCII URL; Paynow rejects addresses with national characters or test domains.- Amounts are integers in minor units (grosze).
Development
composer install
composer ci # pint --test, phpstan (level 8), pest
Author
Created and maintained by Kamil Ćwiertnia, beziworld.eu.
- E-mail: biuro@beziworld.eu
- Issues: GitHub issues
- Security vulnerabilities: see SECURITY.md
Need help integrating payments in your shop? Get in touch through beziworld.eu.
Other packages by the author
laravel-przelewy24 · laravel-tpay · laravel-payu · laravel-autopay · laravel-inpost · laravel-company-data · laravel-smsapi
License
MIT. See LICENSE.md. This package is not affiliated with mBank S.A. (Paynow).