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

Yookassa Sdk Php Laravel Package

yoomoney/yookassa-sdk-php

PHP SDK for integrating YooKassa (YooMoney) payments into your app. Create payments, process refunds, manage receipts, handle webhooks, and call API methods through a convenient client. Simplifies checkout and payment workflows in PHP and Laravel projects.

Deep Wiki
Context7

Getting Started

What you're installing. yoomoney/yookassa-sdk-php is the official PHP SDK for ЮKassa (YooKassa / Yandex.Checkout). It wraps the Payments REST API: creating payments, capturing, cancelling, refunds, and 54‑ФЗ receipts. It is a thin, largely DTO + request-builder layer over Guzzle/PSR‑18, so it does not bring Laravel integration, config, migrations, or a fake — you wire all of that yourself.

Reality check before you start. The package is marked archived, last release 2022‑03‑22. It still works on modern PHP and the API it talks to is stable, but nothing is being patched — no PHP 8.2/8.3 fixes, no security releases, no new API features. Treat it as a transport you own, not a dependency you trust blindly. Pin an exact version, wrap every call behind your own interface, and keep the migration option open.

Minimal setup.

composer require yoomoney/yookassa-sdk-php:^3.0

Config — keep credentials out of code and out of git:

// config/services.php
'yookassa' => [
    'shop_id'    => env('YOOKASSA_SHOP_ID'),
    'secret_key' => env('YOOKASSA_SECRET_KEY'),
    'return_url' => env('YOOKASSA_RETURN_URL'),
],
// config/yookassa.php
return [
    'shop_id'    => env('YOOKASSA_SHOP_ID'),
    'secret_key' => env('YOOKASSA_SECRET_KEY'),
    'return_url' => env('YOOKASSA_RETURN_URL'),
    // YooKassa publishes webhook source IPs; put them here, not inline
    'webhook_ips' => array_filter(explode(',', env('YOOKASSA_WEBHOOK_IPS', ''))),
];

Where to look first. The YooKassa\Client class is the whole surface area: createPayment(), getPaymentInfo(), capturePayment(), cancelPayment(), createRefund(), setAuth(), setLogger(), setHttpClient(). Everything else (YooKassa\Request\Payments\*, YooKassa\Model\*) exists to build arguments and read responses. Note that namespaces shifted between v2 and v3 (YooKassa\Model\Payment vs YooKassa\Model\Payment\Payment, PaymentInterface locations, etc.) — most "class not found" reports online are a v2/v3 mismatch. Check composer show yoomoney/yookassa-sdk-php before copying snippets.

First use case — the redirect payment. This is 90% of day-one work: the user clicks "Pay", they leave your site for the YooKassa confirmation page, they come back, and a webhook fires.

use YooKassa\Client;
use YooKassa\Request\Payments\CreatePaymentRequest;

$client = new Client();
$client->setAuth(config('yookassa.shop_id'), config('yookassa.secret_key'));

$request = CreatePaymentRequest::builder()
    ->setAmount(1490.00)                       // float or ['value' => '1490.00', 'currency' => 'RUB']
    ->setCapture(true)                         // auto-capture; false => you must capture manually
    ->setConfirmation([
        'type'       => 'redirect',
        'return_url' => route('checkout.return', $order),
    ])
    ->setDescription("Order #{$order->id}")
    ->setMetadata(['order_id' => $order->id])  // comes back on webhooks — use it as your join key
    ->build();

$payment = $client->createPayment($request, (string) Str::uuid());
$order->update(['yookassa_payment_id' => $payment->getId()]);

return redirect()->away($payment->getConfirmation()->getConfirmationUrl());

That's the whole starting loop: persist the payment id, redirect, handle the webhook, handle the return URL. Everything below is refinement.


Implementation Patterns

Bind the client as a singleton, never new it in controllers

Client is stateless but re-authenticates and re-builds a Guzzle stack on construction. One instance per request, injected:

// app/Providers/YooKassaServiceProvider.php
public function register(): void
{
    $this->app->singleton(Client::class, function () {
        $client = new Client();
        $client->setAuth(config('yookassa.shop_id'), config('yookassa.secret_key'));
        $client->setLogger(app(LoggerInterface::class));   // PSR-3, ships with the SDK
        return $client;
    });

    $this->app->alias(Client::class, 'yookassa');
}

Better still, wrap it in your own PaymentGateway interface and bind that. You will want to swap it for a maintained SDK or a raw HTTP client eventually, and the archived status makes that a "when", not "if".

Treat payment status as a state machine you own

Don't derive order status from payment->getStatus() on every read — persist it and transition explicitly. YooKassa statuses: pending → (waiting_for_capture →) succeeded, or canceled. Write a small PaymentStatus backed enum and a single applyPaymentState(Order $order, PaymentInterface $payment) method that is idempotent. Both the return-URL controller and the webhook controller call it. This kills the "user refreshed the return URL and got charged twice / fulfilled twice" class of bug.

Idempotence keys are mandatory, so generate and persist them

Every POST on the API requires an Idempotence-Key header. The SDK takes it as the second argument:

$idempotenceKey = $order->yookassa_idempotence_key ??= (string) Str::uuid();
$payment = $client->createPayment($request, $idempotenceKey);

Store it on the order (unique index). If a job retries, the same key means the API returns the original payment instead of creating a second charge. Never generate the key inside a retried closure — that's how double charges happen.

Capture and cancel flows

If you use setCapture(false) (two-step, common for physical goods), funds are held and you have a window (~7 days) to capture:

$client->capturePayment(
    (new CapturePaymentRequest())->setAmount(1490.00),
    $paymentId,
    (string) Str::uuid()
);
// or
$client->cancelPayment($paymentId, (string) Str::uuid());

Always guard: capture only from waiting_for_capture, cancel only from waiting_for_capture or pending. An InvalidRequestException here usually means your local state drifted — log the API body and re-fetch via getPaymentInfo().

Webhooks: parse, then re-fetch, then trust nothing

YooKassa webhooks are not signed. There is no HMAC header. The two things you can do:

  1. IP allowlist — YooKassa publishes its notification source IPs; check $request->ip() against config('yookassa.webhook_ips').
  2. Re-fetch the payment via the API before acting. This is the only authoritative check.
use YooKassa\Model\Notification\NotificationFactory;

public function __invoke(Request $request, Client $client)
{
    abort_unless(in_array($request->ip(), config('yookassa.webhook_ips'), true), 403);

    $notification = (new NotificationFactory())->factory($request->json()->all());
    $paymentId = $notification->getObject()->getId();

    // Authoritative read — the webhook body is a hint, not a fact
    $payment = $client->getPaymentInfo($paymentId);

    if ($payment->getStatus() === 'succeeded') {
        FulfillOrder::dispatch($payment->getMetadata()['order_id'])->onQueue('fulfillment');
    }

    return response()->noContent();
}

Return 200 fast and do the work in a queued job. YooKassa retries non-2xx responses, and slow handlers cause duplicate deliveries. Make the job idempotent (unique payment id / ShouldBeUnique).

The return URL is not a reliable signal — users close tabs, and the webhook may arrive before or after the redirect. The return controller should render a "processing" page and poll, or call applyPaymentState again and let the state machine dedupe.

Receipts (54‑ФЗ) when you're the seller

If you're legally the merchant of record, the payment request needs a receipt or the API rejects it. Build Receipt with ReceiptItems — this is fiddly (tax system, VAT code per item, payment_subject/payment_mode) and belongs in a dedicated factory class, not inline:

$receipt = (new Receipt())
    ->setCustomer(['email' => $order->email])
    ->setTaxSystemCode(1)
    ->addItem(
        (new ReceiptItem())
            ->setDescription(mb_substr($item->name, 0, 128))
            ->setQuantity($item->qty)
            ->setAmount($item->lineTotal)
            ->setVatCode(1)
    );
$request->setReceipt($receipt);

Test this path in the sandbox before production — receipt validation errors are opaque (400 invalid_request with a parameter field buried in the body).

Refunds

Refunds are their own resource and are asynchronous:

$refund = $client->createRefund(
    (new CreateRefundRequest())->setPaymentId($paymentId)->setAmount(500.00),
    (string) Str::uuid()
);

Partial refunds are allowed up to the captured amount. Track cumulative refunds yourself — the API will let you attempt a second partial that exceeds the total and reject it, but you want to catch that before the user sees an error.

HTTP layer and testing

The SDK accepts a PSR‑18 client via setHttpClient(), which is your seam for tests. Two workable approaches:

  • Mock the Client in your gateway wrapper and unit-test your state machine / controllers.
  • Inject a fake PSR‑18 client returning canned JSON when you want to exercise the SDK's own parsing.

There is no Laravel HTTP fake support natively; don't try to Http::fake() the SDK — it doesn't use the Laravel HTTP client. Use the separate test shop (shop_id + test_ secret key) in a sandbox environment to get real end-to-end behaviour without moving money.


Gotchas and Tips

Money is a string, and floats will bite you. The SDK serialises amounts as "1490.00". Passing floats and doing arithmetic on them produces 1489.9999 and invalid_amount errors. Store money as integer kopecks in your DB, divide once at the boundary, and format with number_format($kopecks / 100, 2, '.', ''). Do not round-trip through the SDK's getAmount()->getValue() for your own accounting.

Currency is effectively RUB. currency defaults to RUB and most shops can only accept it. If you're multi-currency, verify with YooKassa support before assuming it works — the SDK won't warn you.

idempotenceKey must be ≤ 64 chars and unique per operation. Reuse for retries of the same operation, new key for a new payment/refund/capture. A single global key per order breaks partial refunds.

Never assume the webhook ordering. payment.succeeded and payment.canceled can arrive out of order or duplicated. Your state machine must be monotonic: succeeded is terminal, canceled from succeeded should be logged and ignored.

The archived-package tax is real. Expect:

  • PHP 8.1+ deprecation notices from implicit nullables and missing return types in older minor versions.
  • No support for newer API fields (e.g. newer payment methods, SBP/deal parameters added after 2022) — you'd have to hand-roll raw requests via $client->getApiClient() or drop to cURL.
  • No security advisories. Run composer audit and accept that findings will never be fixed upstream.

Mitigation: keep the SDK behind your own PaymentGateway interface with 4–5 methods (createRedirectPayment, fetch, capture, cancel, refund). That's a day of work and it's the difference between a one-file migration and a two-week one.

Connection vs. business errors are different. Catch YooKassa\Common\Exceptions\ApiConnectionException (and 5xx) and retry with backoff — safe because of idempotence keys. Do not retry BadApiRequestException / InvalidRequestException (4xx): the request is wrong, retrying just burns rate limit. Log the full response body; the SDK's exception messages are terse and the useful parameter / code is in the JSON.

Timeouts default generously. Set an explicit HTTP timeout (10–15s) on your custom PSR‑18 client. A hung YooKassa call inside a synchronous checkout request is a bad user experience and a worse queue worker.

confirmation_url expires. Redirect immediately; don't email it or cache it. If the user abandons and returns hours later, create a new payment (new idempotence key) rather than reusing the URL.

Metadata round-trips. setMetadata(['order_id' => ...]) is the cleanest way to correlate webhooks to your domain without a lookup table on payment id. Metadata is limited (roughly 32 keys, string values) — cast to string.

Configuration quirks.

  • setAuth() can also take a second arg for OAuth, but stick to shopId + secret key.
  • The secret key in test mode starts with test_; production keys don't. Add a boot-time assertion so a test key never ships to prod (and vice versa).
  • The SDK reads no env vars itself — everything is explicit, which is good, but means a missing config('yookassa.shop_id') fails with a confusing 401 rather than a clear config error. Validate in a service provider's boot().

Extension points worth knowing. setLogger(Psr\Log\LoggerInterface) gives you request/response visibility when debugging 400s — turn it to debug in staging and warning in prod. setApiEndpoint() lets you point at a mock server for integration tests. setHttpClient() is your path to custom middleware (tracing, retries).

Migration horizon. Given the archive date, budget for replacing this within a year or two — either the maintained upstream (if YooKassa resumes releases under a new name) or a thin in-house HTTP client against the well-documented REST API. The API itself is the stable contract; this SDK is just a convenience that's stopped being convenient.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core