Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Libwebtopay Laravel Package

webtopay/libwebtopay

The WebToPay PHP library integrates Paysera’s Checkout API for collecting online payments via many methods and validating transferred/received security parameters. Install via Composer or the all-in-one WebToPay.php file.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the version that matches your PHP runtime, and pin it — the package has very little ecosystem adoption, so floating constraints buy you nothing but surprise:

composer require webtopay/libwebtopay:^3.0   # PHP 7.4+, strict_types + type hints
# older apps: ^2.0 (new interface) or ^1.6 (legacy interface)

Verify the global class is autoloadable before writing anything else:

php artisan tinker --execute="var_dump(class_exists(\WebToPay::class));"

If it is false, run composer dump-autoload; the library ships a merged all-in-one WebToPay.php and an src/includes.php, either of which can be required as a fallback — but only once, never alongside the Composer autoload.

Where to look first:

  • vendor/webtopay/libwebtopay/src/ — the actual WebToPay facade class, WebToPayException, signature/encoding helpers, and (depending on version) WebToPay_Factory / payment-method list classes. The method signatures there are the source of truth for your installed version.
  • The README points at developers.paysera.com/en/checkout/basic — that is where the parameter contract lives: projectid, orderid, amount, currency, country, payment, accepturl, cancelurl, callbackurl, test, lang, time_limit.
  • Your Paysera project settings: project ID, sign password, and whether the project is in test mode.

First use case: a one-off payment for an order. Put credentials in config/services.php, then build the redirect URL and hand it to Laravel's redirector:

// config/services.php
'paysera' => [
    'project_id'    => env('PAYSERA_PROJECT_ID'),
    'sign_password' => env('PAYSERA_SIGN_PASSWORD'),
    'test'          => (bool) env('PAYSERA_TEST', false),
],
// app/Http/Controllers/PayseraController.php
public function pay(Order $order)
{
    $url = \WebToPay::getPaymentUrl([
        'projectid'     => config('services.paysera.project_id'),
        'sign_password' => config('services.paysera.sign_password'),
        'orderid'       => (string) $order->payment_attempt_id,
        'amount'        => $order->amount_minor,      // integer, minor units
        'currency'      => $order->currency,          // 'EUR'
        'country'       => 'LT',
        'accepturl'     => route('paysera.accept'),
        'cancelurl'     => route('paysera.cancel'),
        'callbackurl'   => route('paysera.callback'),
        'test'          => config('services.paysera.test') ? 1 : 0,
    ]);

    return redirect()->away($url);
}

Prefer getPaymentUrl() over WebToPay::redirectToPayment() — the latter emits headers and terminates the request itself, stepping outside Laravel's response pipeline (and making feature tests miserable).

Implementation Patterns

Wrap the static class, don't call it everywhere. You need a seam for tests and a place to normalize config:

namespace App\Services;

use WebToPay;

class PayseraGateway
{
    public function __construct(private readonly array $config) {}

    public function paymentUrl(array $params): string
    {
        return WebToPay::getPaymentUrl(array_merge([
            'projectid'     => $this->config['project_id'],
            'sign_password' => $this->config['sign_password'],
            'test'          => $this->config['test'] ? 1 : 0,
        ], $params));
    }

    public function parse(array $query): array
    {
        // v2/v3: checkResponse() takes the query array + options
        return WebToPay::checkResponse($query, [
            'projectid'     => $this->config['project_id'],
            'sign_password' => $this->config['sign_password'],
        ]);
        // v1.x/legacy signature, if you are stuck on it:
        // return WebToPay::validateAndParseData($query, $this->config['sign_password'], 'ss1', 'ss2');
    }
}

Bind it as a singleton in a service provider with config('services.paysera'), and in tests bind a fake behind an interface.

Model the payment attempt, not the order. Paysera's orderid must be your own identifier and it must round-trip unchanged through the callback. Create a payment_attempts row (or use a dedicated column) before redirecting, store the amount/currency you sent, and look the row up by orderid on callback. Never derive fulfillment from the browser's return trip.

Three routes, three jobs. accept and cancel are user-facing: validate the signature, then render "thank you / pending / failed" — but do not mark the order paid there. callback is server-to-server and is the only authoritative confirmation:

public function callback(Request $request)
{
    try {
        $data = $this->paysera->parse($request->query());
    } catch (\WebToPayException $e) {
        report($e);
        return response('', 400); // invalid signature: do not invite retries
    }

    DB::transaction(function () use ($data) {
        $attempt = PaymentAttempt::where('external_id', $data['orderid'])
            ->lockForUpdate()
            ->firstOrFail();

        abort_unless((int) $data['amount'] === $attempt->amount_minor, 422);
        abort_unless($data['currency'] === $attempt->currency, 422);

        match ((int) $data['status']) {
            \WebToPay::PAYMENT_STATUS_SUCCESS => $attempt->markPaid($data),
            \WebToPay::PAYMENT_STATUS_FAILED  => $attempt->markFailed($data),
            \WebToPay::PAYMENT_STATUS_WAITING => $attempt->markWaiting($data),
            default                           => $attempt->markWaiting($data),
        };
    });

    return response('OK', 200)->header('Content-Type', 'text/plain');
}

Dispatch an event / queued job from the status transition (PaymentSucceeded) so fulfillment, receipts, and webhooks are idempotent and out of the request cycle. Status transition methods should be no-ops when the target state is already reached.

CSRF and auth. The callback has no session and no token. In Laravel 10 and below, add 'paysera/callback' to VerifyCsrfToken::$except (or apply withoutMiddleware). In Laravel 11+, ->withMiddleware(fn ($m) => $m->validateCsrfTokens(except: ['paysera/*'])) in bootstrap/app.php. Put the callback in routes/web.php if you need session/signed URLs elsewhere, in routes/api.php if you want it fully stateless — either way keep the URL stable, because it is baked into what Paysera sends back.

Absolute URLs. route() inside a queued job or console command can inherit the wrong host. Pass accepturl/cancelurl/callbackurl built from an explicit base (config('app.url') or a dedicated services.paysera.base_url) so staging and production never cross wires.

Payment methods. If you let users preselect a bank, store the Paysera method code in a payment_methods mapping seeded from the docs and pass it as payment. If you use the library's payment-method list classes, cache the result heavily — it changes rarely and is a network call inside the checkout path.

Gotchas and Tips

  • Amounts are integers in minor units. 10.00 EUR is 1000. Passing a float, a decimal string, or a formatted number will either fail or charge the wrong value. Store amount_minor as an unsigned big integer plus ISO currency and never touch floats.
  • Do not rebuild the query string. Paysera sends data, ss1 (and ss2) — the signature is computed over the urlencoded payload. Laravel has already URL-decoded once; re-encoding, sorting, or cherry-picking parameters breaks verification. Pass $request->query() (or $request->all() if you must support POST) straight through, never http_build_query() output that you re-parsed.
  • + in base64. Query-string + becomes a space in PHP. If you see "invalid signature" only for some callbacks, this is usually why — the library's decoder handles the properly encoded form, so make sure nothing upstream (proxy rewrites, WAF, your own middleware) mangles the raw query.
  • redirectToPayment() sends headers and exits. It bypasses middleware, response macros, and test assertions. Always use getPaymentUrl().
  • Self-URL detection behind proxies. Some versions infer the current URL from $_SERVER. Configure TrustProxies correctly and pass explicit absolute URLs so you never depend on that inference.
  • Callbacks repeat and can arrive out of order. Waiting (0) may arrive before success (1), and success may be delivered several times. Make the handler idempotent, always re-read status inside a transaction with lockForUpdate(), and return OK/200 for already-processed callbacks so Paysera stops retrying. Return a 5xx only for genuinely transient failures (DB down) where a retry helps.
  • Compare payamount, not just amount. The callback carries the requested amount and the actually paid amount/currency. Validate both against your stored attempt before fulfilling; a mismatch is a fraud/ops signal, not a rounding issue.
  • Test mode is a parameter, not a project switch. test=1 marks the payment as a test; the callback echoes test back. Log it, and make sure production fulfillment code refuses test-flagged callbacks (or at least flags them loudly).
  • accepturl fires before the callback sometimes. Users see the "success" page while your DB still says pending. Render "we're confirming your payment" and poll or refresh, rather than promising a completed order.
  • Exceptions are the API. Every validation problem surfaces as WebToPayException (with subclasses in some versions). Catch it narrowly, log the payload minus secrets, alert on signature failures (they usually mean misconfiguration, not attacks), and never echo the exception message to the payer.
  • sign_password never goes in a URL. It is only an input to signature computation. Keep it in .env, out of logs, out of config:cache-visible dumps, and out of exception messages.
  • Locale and country. lang and country are cosmetic/pre-selection hints on Paysera's page; map them from app()->getLocale() with a small hardcoded table and default gracefully rather than sending an unsupported value.
  • Low-adoption dependency. Eleven stars and zero dependents means no community safety net. Pin the exact version in composer.json, keep the library unmodified, and write contract tests around your PayseraGateway (one sandbox integration test for a real signature round-trip; fakes everywhere else). To generate a valid fixture callback in tests without the network, use the library's own sign/encode helpers in src/ rather than hand-rolling base64 + md5.
  • License. LGPL-3.0 is fine for typical SaaS use via Composer (dynamic linking), but if you patch files inside vendor/ you are on the hook to publish those changes. Prefer decorating the library over editing it.
  • Debugging checklist for a failing payment: (1) is projectid a string and correct for the environment, (2) does test match the project's mode, (3) is amount an integer in minor units, (4) do accepturl/cancelurl/callbackurl resolve to publicly reachable HTTPS URLs, (5) did any middleware touch the callback query string. Paysera's redirect page shows errors as human-readable text, not API responses, so log the outgoing parameter array (without sign_password) on every initiation.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib