jeka/qiwi-api
Lightweight PHP/Laravel wrapper for the QIWI Wallet API: check balances, send payments, and manage invoices with a simple, readable client and straightforward configuration. A convenient way to add QIWI support to small projects and prototypes.
composer require jeka/qiwi-api
This is a plain PHP SDK, not a Laravel package — there is no service provider, facade, or publishable config. You wire it up yourself, which takes about ten minutes and is genuinely the easy part.
Where to look first:
README.md on the GitHub repo for the client constructor signature and the list of available methods. That file is your only documentation; there is no hosted docs site.src/ end to end. The package is small enough to read in 10–15 minutes, and you should, because you will be maintaining this code.composer.json for the PHP and illuminate/* constraints before committing to anything. Low-activity packages routinely lag behind current PHP 8.x and Laravel 11/12 releases.Credentials. Get a QIWI API token from https://qiwi.com/api (Settings → API Access) and whitelist your server's outbound IPs — QIWI rejects requests from unrecognized addresses with an unhelpful error. The partner/billing API uses a site_id + secret pair instead. Either way, put it in .env and never in the repo.
First use case: a balance check or recent-payments listing. It is read-only, moves no money, and validates your token and IP whitelist before you attempt anything transactional. Once that returns, you know auth works and can start on invoices.
// app/Providers/QiwiServiceProvider.php
$this->app->singleton(\Jeka\QiwiAPI\Client::class, fn () => new \Jeka\QiwiAPI\Client(
config('services.qiwi.token'),
));
Bind the client as a singleton. Each instantiation usually carries auth headers and its own HTTP client. Let the container own it so you get connection reuse and one place to inject Guzzle options, timeouts, and middleware for logging.
Wrap it in a domain service. Never call the SDK from a controller. Create App\Services\QiwiService that exposes intent-shaped methods (createInvoice, getPaymentStatus, refund) and returns your own DTOs. This is the single most important pattern here — the SDK is thin and likely to be abandoned, so you want exactly one seam to swap when you move to raw Guzzle, a maintained fork, or another provider.
class QiwiService
{
public function __construct(private \Jeka\QiwiAPI\Client $client) {}
public function createInvoice(string $billId, int $amountRub, string $comment): Invoice
{
// map to the SDK call, validate the response, return your own DTO
}
}
Invoice creation is keyed by a caller-supplied bill_id. Generate it at the business layer as a UUID (or order-{id}-{uuid}), persist it on your invoices table with status pending, then call the API. That gives you idempotency for free: a retry after a network timeout resends the same bill_id rather than creating a second bill.
Reconciliation is poll-based. QIWI does not push reliable webhooks for every account tier. The realistic pattern is a scheduled job that pulls open invoices and refreshes their status:
// bootstrap/app.php or routes/console.php
Schedule::job(new ReconcileQiwiInvoices)->everyFiveMinutes();
Have the job select invoices in pending younger than N hours, call the status endpoint, and update local state. Drive every transition through a single markPaid() / markFailed() method so domain events (PaymentSucceeded, PaymentFailed) dispatch from one place.
If your account does support webhooks, treat the handler as a trigger, not a source of truth. Always re-fetch the bill's authoritative status from the API before flipping local state, and make the handler idempotent — QIWI retries. Verify whatever signature or shared secret your merchant tier provides; do not accept the payload body on trust.
Queue every write. Anything that creates a bill, refunds, or pays out goes on a queued job with a timeout and bounded retries. Keep the HTTP call out of the request path so a slow or failing QIWI response doesn't stall checkout. Inside the job, catch transport exceptions, log with the bill_id, and let the reconciler recover anything that timed out.
Log with redaction. The SDK may serialize request payloads into exceptions. Wrap calls in a try/catch that logs method, endpoint, HTTP status, and bill_id — and explicitly strips the token and any full request body before it hits your log aggregator.
composer.json doesn't accept your PHP/Laravel version, stop and decide between a fork and raw Guzzle. Don't force it with --ignore-platform-reqs.QiwiService interface with Laravel's Http client when you need streaming, retry middleware, or a better testing story. Http::fake() is far nicer than mocking a third-party SDK.QiwiService at a fake in tests so the suite doesn't depend on QIWI's sandbox availability, and keep a small opt-in integration test suite for pre-release checks.How can I help you explore Laravel packages today?