Search by

beziworld / laravel-payu

kamcw

PayU REST API integration for Laravel: orders, signed notifications, capture, refunds and payment methods.

Package info

github.com/Beziworld/laravel-payu

Homepage

Issues

pkg:composer/beziworld/laravel-payu

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

v1.0.0 2026-11-04 14:25 UTC

This package is auto-updated.

Last update: 2026-11-04 16:36:38 UTC


README

tests Latest Version License

PayU REST API v2.1 for Laravel: orders, BLIK, signed notifications, capture, cancel, refunds and payment methods, with a cached OAuth token. Verified against the PayU sandbox.

Polska wersja dokumentacji

Installation

composer require beziworld/laravel-payu
php artisan vendor:publish --tag=payu-config   # optional
PAYU_POS_ID=300746
PAYU_SECOND_KEY=b6ca15b0d1020e8094d9b5f8d163db54      # "Second key (MD5)"
PAYU_CLIENT_ID=300746                                 # defaults to the POS id
PAYU_CLIENT_SECRET=2ee86a66e5d97e3fadc400c9f19b065d
PAYU_SANDBOX=true
PAYU_CONTINUE_URL=https://shop.example.com/payment/return

The values above are the public PayU sandbox POS. The notification URL defaults to the package route /payu/notification.

Payment flow

use Beziworld\PayU\Data\OrderRequest;
use Beziworld\PayU\Data\Product;
use Beziworld\PayU\Facades\PayU;

public function pay(Request $request, Order $order): RedirectResponse
{
    $created = PayU::createOrder(new OrderRequest(
        totalAmount: $order->total_in_grosze,
        description: "Order {$order->number}",
        customerIp: $request->ip(),
        products: $order->items->map(fn ($item) => new Product($item->name, $item->price_in_grosze, $item->quantity))->all(),
        extOrderId: $order->number,
        buyer: ['email' => $order->email, 'firstName' => $order->first_name, 'lastName' => $order->last_name, 'language' => 'pl'],
    ));

    $order->update(['payu_order_id' => $created->orderId]);

    return redirect()->away($created->redirectUri);
}

Notifications

use Beziworld\PayU\Events\OrderCompleted;

Event::listen(function (OrderCompleted $event) {
    $order = Order::where('number', $event->order->extOrderId)->firstOrFail();

    if (! $order->isPaid() && $event->order->totalAmount === $order->total_in_grosze) {
        $order->markAsPaid(payuPaymentId: $event->paymentId);
    }
});

Other events: OrderStatusChanged (every order notification) and RefundStatusChanged. The OpenPayu-Signature header (MD5 or SHA-2) is verified over the raw body with the second key. PayU repeats notifications until it receives HTTP 200, so listeners must be idempotent. Optionally restrict the sender with payu.notifications.allowed_ips.

BLIK level 0

PayU::createOrder(new OrderRequest(
    // ...
    payMethod: ['type' => 'BLIK_AUTHORIZATION_CODE', 'value' => 'blik', 'authorizationCode' => $request->input('blik_code')],
));

Capture, cancel, refunds

PayU::order($orderId)->status;             // OrderStatus::Completed
PayU::capture($orderId);                   // for POS without automatic capture (WAITING_FOR_CONFIRMATION)
PayU::cancel($orderId);                    // release the funds of an uncompleted order
PayU::refund($orderId, 'Return', 2500, "refund-{$return->id}");   // amount omitted = full refund
PayU::paymentMethods('pl');                // list<PaymentMethod> with logos and amount limits

extRefundId makes refunds idempotent: repeating the call with the same id does not refund twice.

Exceptions

ApiException (statusCode(), codeLiteral(), httpStatus(), payload()), InvalidNotification and InvalidConfiguration, all extending PayUException.

Development

composer install
composer ci   # pint --test, phpstan (level 8), pest

Author

Created and maintained by Kamil Ćwiertnia, beziworld.eu.

Need help integrating payments in your shop? Get in touch through beziworld.eu.

Other packages by the author

laravel-przelewy24 · laravel-tpay · laravel-paynow · laravel-autopay · laravel-inpost · laravel-company-data

License

MIT. See LICENSE.md. This package is not affiliated with PayU S.A.