mnastalski/przelewy24-php
Przelewy24 payment gateway integration for PHP and Laravel. Register transactions, verify payments, handle webhooks, and manage refunds through a clean API, making Polish online payments easier to add to your application.
This is a framework-agnostic SDK, not a Laravel package — there is no auto-discovery, no service provider, and no facade. Plan on writing ~30 lines of glue code the first time and reusing it everywhere after.
Minimal setup:
composer require mnastalski/przelewy24-php
merchantId (posId), crc key, apiKey (for REST auth), and reportKey. Sandbox credentials are separate and issued from sandbox.przelewy24.pl.config/services.php:'przelewy24' => [
'merchant_id' => env('P24_MERCHANT_ID'),
'crc' => env('P24_CRC'),
'api_key' => env('P24_API_KEY'),
'report_key' => env('P24_REPORT_KEY'),
'sandbox' => env('P24_SANDBOX', true),
],
AppServiceProvider::register():$this->app->singleton(Client::class, fn ($app) => new Client(
config('services.przelewy24.merchant_id'),
config('services.przelewy24.crc'),
config('services.przelewy24.api_key'),
config('services.przelewy24.report_key'),
config('services.przelewy24.sandbox'),
));
Order, call the SDK's transaction registration, persist the returned token, and redirect the user to the P24 payment URL. The notification/return flow is where most integration bugs live, so build the webhook handler at the same time rather than later.Where to look first in the repo: the README.md (short but the only usage reference), then src/Client.php and the resource classes it exposes (Transaction, Payment, Report-style namespaces). The tests folder is often more illustrative than the README for request/response shapes.
Wrap the SDK in an app-level service. Don't scatter new Client(...) or raw calls through controllers. A thin PaymentService that takes the singleton and exposes register(), verify(), refund() keeps the SDK swappable and gives you one place to add logging and error translation.
Register-then-redirect flow. Always commit the order to the database before calling the gateway. Store the SDK's returned token and the session ID alongside the order. If registration fails, the order exists in pending rather than not at all.
$order = Order::create([...]);
$response = $this->p24->transaction()->register([
'sessionId' => (string) $order->id, // must be unique per P24 session
'amount' => $order->total_grosze, // integer, smallest currency unit
'currency' => 'PLN',
'description' => "Order #{$order->id}",
'email' => $user->email,
'country' => 'PL',
'language' => 'pl',
'urlReturn' => route('checkout.return'),
'urlStatus' => route('webhooks.p24'),
'timeLimit' => 15,
]);
$order->update(['p24_token' => $response->token]);
return redirect($response->redirectUrl);
Webhook handler = verify + confirm. The urlStatus callback is unauthenticated HTTP from P24. Recompute the sign with your CRC, call the SDK's verify() to confirm the amount/currency/session against the gateway (don't trust the payload), then mark the order paid. This "confirmation" step is mandatory in P24's model — skipping it leaves transactions unsettled even though the user was charged.
Use a queued job for the confirmation call, keyed on sessionId with a uniqueness lock, so retries and duplicate callbacks don't double-credit an order.
Idempotent notification endpoint. Return 200 quickly, do the work in a job. P24 retries callbacks for a while.
Testing. Bind a fake Client in tests (or a fake PaymentService) so no HTTP leaves the container. For an end-to-end sandbox check, the P24 test-card numbers and the sandbox urlReturn (must be reachable — use a tunnel like Expose/ngrok) are your only true integration test.
config/przelewy24.php out of the box, no php artisan vendor:publish. You own the wiring — do it once in a provider and forget it.19.99 instead of 1999 is the single most common bug. Cast to int at the boundary; never let floats reach the SDK.sessionId must be unique across your merchant account. If you reuse order IDs after a failed attempt, P24 will reject with a duplicate-session error. Prefix with a nonce or use a UUID per attempt.verify() fails and the payment sits unsettled. Snapshot the total at registration and never recompute.P24_SANDBOX=false in production and every request goes to the test environment — orders will "succeed" and charge nothing. Gate this with a runtime check (fail boot if app()->isProduction() && config('...sandbox')).urlReturn and urlStatus must be absolute URLs reachable from P24's servers. localhost works only for urlReturn (browser redirect), never for urlStatus.PaymentException so controllers stay clean and logs carry the P24 error code/message — those messages are the fastest debugging path.redirectUrl vs url, token vs sessionId). Print $response once in tinker in the sandbox before trusting autocompletion — the SDK is loosely typed and IDE hints will lie.reportKey for reports). They're handy for nightly reconciliation jobs: pull the day's settlements into a p24_settlements table and diff against orders to catch drift.composer.json and read the changelog before upgrading — enum/response shape changes have shipped without major bumps historically.How can I help you explore Laravel packages today?