evp/paysera-wallet-php-client
A PHP client for the Paysera Wallet API that simplifies authentication, payment initiation, and callback handling. Integrates cleanly into Laravel and plain PHP projects, giving developers a straightforward, object-oriented wrapper for Paysera e-wallet transactions.
composer require evp/paysera-wallet-php-client guzzlehttp/guzzle
There is no service provider, no config file to publish, and no facade. You wire it yourself in config/services.php:
// config/services.php
'paysera_wallet' => [
'client_id' => env('PAYSERA_WALLET_CLIENT_ID'),
'client_secret' => env('PAYSERA_WALLET_CLIENT_SECRET'),
'redirect_uri' => env('PAYSERA_WALLET_REDIRECT_URI'),
'host' => env('PAYSERA_WALLET_HOST', 'https://api.paysera.com/wallet/rest/v1'),
],
Where to look first. This is an OpenAPI-generated client, so the layout is predictable:
composer.json → autoload.psr-4 gives you the real root namespace (it is not always what you guess — confirm before writing use statements).src/Configuration.php → host, auth, user-agent, debug toggle. This is your main integration point.src/Api/*Api.php → one class per REST tag (WalletApi, TransferApi, PaymentApi, …). Each public method is one endpoint and its docblock tells you the exact request/response models.src/Model/ → DTOs. Read the model for the endpoint you need; do not guess field names from the Paysera docs, they drift.First use case: a read-only smoke test. Get a token, call the cheapest "list wallets" or "get balance" endpoint, and dump the response. This validates credentials, host, and PSR-18 wiring in one shot before you touch money movement.
// app/Providers/AppServiceProvider.php
use Paysera\WalletApi\Configuration;
use Paysera\WalletApi\Api\WalletApi;
$this->app->bind(WalletApi::class, function () {
$config = (new Configuration())
->setHost(config('services.paysera_wallet.host'))
->setAccessToken(Cache::get('paysera.wallet.token'));
return new WalletApi(new \GuzzleHttp\Client(['timeout' => 15]), $config);
});
// tinker / a throwaway route
$wallets = app(WalletApi::class)->getWallets();
dump($wallets); // or ObjectSerializer::sanitizeForSerialization($wallets)
OAuth token handling is your job. The generated client accepts an access token via Configuration::setAccessToken(); it does not run the authorization-code flow for you. For user-scoped wallets, build the consent URL yourself (/oauth/v1/authorize on the wallet host, with client_id, redirect_uri, scope, state), exchange the code at /oauth/v1/token, and persist the token pair against your User model with an encrypted cast. For server-to-server jobs use client credentials and cache the token with a TTL slightly under expires_in.
Never bind a token-bearing client as a singleton. Tokens are per-user, so bind a factory and resolve per request/job:
final class PayseraWalletFactory
{
public function forUser(User $user): WalletApi
{
$token = $this->tokenService->validToken($user); // refreshes if <60s left
return new WalletApi(
app(\GuzzleHttp\ClientInterface::class),
(new Configuration())
->setHost(config('services.paysera_wallet.host'))
->setAccessToken($token->access_token),
);
}
}
Wrap the generated Api classes. They throw generic ApiException and return raw DTOs. Put them behind a PayseraWallet service that maps DTOs to your own value objects/exceptions. This is a 21-star, vendor-generated library — the adapter boundary is what makes it replaceable later.
Outbox pattern for money movement. Never call a transfer endpoint directly from a controller. Write your transfers row and a SendPayseraTransfer job inside one DB::transaction, then let the queue do the HTTP call. On retry, reuse the same idempotency key / client-side transaction ID you stored on the row, otherwise retries can double-send.
final class SendPayseraTransfer implements ShouldQueue
{
use Queueable;
public int $tries = 5;
public array $backoff = [5, 15, 60, 300];
public function __construct(public Transfer $transfer) {}
public function handle(PayseraWallet $wallet): void
{
$wallet->transfer(
$this->transfer->user,
amount: $this->transfer->amount_minor, // integer minor units
currency: $this->transfer->currency,
idempotencyKey: $this->transfer->uuid, // stable across retries
);
}
public function failed(Throwable $e): void
{
$this->transfer->update(['status' => 'failed', 'error' => $e->getMessage()]);
}
}
Webhooks. Register a route outside web middleware (or withoutMiddleware(VerifyCsrfToken::class)), return 200 immediately, and dispatch a job. Verify the signature header before trusting anything, and dedupe on the event ID the payload carries — Paysera will redeliver.
Testing. Because the client is PSR-18, Http::fake() does nothing. Use Guzzle's MockHandler and inject the handler, or bind a fake PayseraWallet interface in the container — the latter is why the adapter from above matters:
$mock = new \GuzzleHttp\Handler\MockHandler([
new \GuzzleHttp\Psr7\Response(200, [], json_encode(['items' => []])),
]);
$client = new \GuzzleHttp\Client(['handler' => \GuzzleHttp\HandlerStack::create($mock)]);
evp/… but the upstream repo is paysera/lib-wallet-php-client. Confirm the Packagist package actually tracks upstream releases; if it lags, add the upstream as a VCS repository instead of forking."No HTTP client found" means php-http/discovery has nothing to pick. Install guzzlehttp/guzzle and always pass the client explicitly rather than relying on discovery.ApiException is unhelpful by default. Log $e->getCode() (HTTP status) and $e->getResponseBody() (the JSON error body) — the message alone is usually just [422]. Map 401 → refresh token, 429 → backoff job, 4xx → permanent failure (mark the row failed, don't retry), 5xx → retry.Cache::lock("paysera.token.{$user->id}") and re-check expiry inside the lock.ObjectSerializer::sanitizeForSerialization($model) then json_encode to see what you're actually sending.null fields; endpoints that require an explicit null or false (e.g. clearing a field) will silently no-op. Check the generated model's getXAllowNull() / setX() signatures.\DateTime objects, not strings, in both directions. Format at the edge; don't hand a Carbon instance to a strict \DateTime typed setter without converting.HandlerStack with RetryMiddleware for 429/5xx and a logging middleware — that's your only observability hook into the generated code.Configuration (setHost, setUserAgent, setDebug, setAccessToken, setTempFolderPath) and injecting a custom HeaderSelector. Anything else requires subclassing a generated file, which you'll lose on the next update — prefer wrapping.PAYSERA_WALLET_HOST explicit per environment and fail fast on boot if it's missing in production."evp/paysera-wallet-php-client": "1.*") and treat upgrades as a review task — generated clients change method signatures between minor releases more often than hand-written ones.How can I help you explore Laravel packages today?