yandex-money/yandex-checkout-sdk-php
Official PHP SDK for YooMoney (formerly Yandex.Checkout) that simplifies integrating online payments, refunds, and payouts into PHP applications. Includes API client classes, request/response models, and helpers for common payment flows, helping developers accept payments quickly.
yoomoney/yookassa-sdk-php is framework-agnostic — Laravel gives you nothing out of the box, so the first hour is wiring, not reading docs.
composer require yoomoney/yookassa-sdk-php:^1.6.9
Look at these four things first, in this order:
YooKassa\Client — the whole API surface: createPayment(), getPayment(), capturePayment(), cancelPayment(), createRefund(), createPayout().YooKassa\Request\Payments\CreatePaymentRequest — the request object you actually build.YooKassa\Model\PaymentInterface — the response shape and status constants (pending, waiting_for_capture, succeeded, canceled).YooKassa\Model\Transfer and YooKassa\Model\Source — changed in 1.6.9 (platform_fee_amount support).Config + binding:
// config/services.php
'yookassa' => [
'shop_id' => env('YOOKASSA_SHOP_ID'),
'secret_key' => env('YOOKASSA_SECRET_KEY'),
'return_url' => env('YOOKASSA_RETURN_URL'),
],
// AppServiceProvider::register()
$this->app->singleton(Client::class, function ($app) {
$client = new Client();
$client->setAuth(config('services.yookassa.shop_id'), config('services.yookassa.secret_key'));
$client->setLogger($app->make(LoggerInterface::class));
return $client;
});
First use case — redirect payment with a fiscal receipt:
$amount = (new Amount())->setValue('1500.00')->setCurrency('RUB');
$request = (new CreatePaymentRequest())
->setAmount($amount)
->setCapture(true)
->setDescription('Order #1042')
->setConfirmation(new ConfirmationRedirect(config('services.yookassa.return_url')))
->setReceipt((new Receipt())
->addItem((new ReceiptItem())
->setDescription('Widget')
->setQuantity(1)
->setAmount($amount)));
$payment = $client->createPayment($request, (string) Str::uuid());
return redirect($payment->getConfirmation()->getConfirmationUrl());
Store $payment->getId() and correlate it with your order before redirecting — the webhook may arrive before the user comes back.
Wrap the client, don't leak it. Put a YooKassaGateway / PaymentService between controllers and YooKassa\Client. Map SDK models to your own DTOs in one place; the SDK's AbstractObject types are awkward in Blade/JSON responses and you'll want to swap the transport later.
Idempotency keys are your responsibility. There is no automatic key generation in older 1.x builds. Generate a UUID v4 once per logical operation, persist it on the order row, and reuse the exact same key on retry. Same key + different body = 400. Max 64 chars.
Webhook handling. The SDK ships YooKassa\Model\Notification\NotificationFactory for parsing, but there is no signature verification. The supported model is:
$notification = (new NotificationFactory())->factoryFromArray($request->all());
// Never trust the body — re-fetch the source of truth
$payment = $client->getPayment($notification->getObject()->getId());
Then: verify the request IP against YooKassa's published ranges, return 200 immediately, and dispatch() a job that re-fetches and transitions your order state. Webhooks are at-least-once.
Two-phase capture. setCapture(false) → waiting_for_capture → capturePayment($id, $amount, $idempotenceKey) when you ship. Good for pre-orders and stock checks.
Split payments (the 1.6.9 feature). platform_fee_amount lets you express the marketplace commission explicitly instead of inferring it from amount:
$transfer = (new Transfer())
->setAccountId($sellerAccountId)
->setAmount((new Amount())->setValue('1350.00')->setCurrency('RUB'))
->setPlatformFeeAmount((new Amount())->setValue('150.00')->setCurrency('RUB'));
$request->setTransfers([$transfer]); // payment amount must be 1500.00
The same parameter is now available on Source for payout requests (sources[]), where it represents the fee withheld on the payout side.
State machine, not event sourcing. Model orders.status transitions (created → pending → paid → refunded) and treat webhooks as one of several triggers. Add a scheduled reconciliation job that sweeps payments stuck in pending older than N minutes via getPayment() — webhooks will get dropped.
'1350.00'), currency 'RUB'. Compute platform_fee_amount in integer kopecks (or bcmath) and format at the boundary, otherwise 1350.00 + 150.00 != 1500.00 after float rounding and the API rejects the request.transfer.amount + transfer.platform_fee_amount must equal the payment amount. If you only set amount, YooKassa computes the fee as the remainder — so specifying platform_fee_amount is mostly a safety/auditing win, but it also lets you assert the split server-side before the call.platform_fee_amount only works if the shop has split payments enabled and the payment method supports it (bank card, SberPay, SBP — not all methods). Otherwise you get a generic 400 invalid_request. Test in the sandbox against a real shop; a locally-passing test proves nothing here.Transfer::getPlatformFeeAmount() / Source::getPlatformFeeAmount() return AmountInterface|null, and jsonSerialize() omits nulls, so existing payloads are unchanged. But if you pinned 1.6.8 or lower, setPlatformFeeAmount() is an undefined method — bump the constraint to ^1.6.9 explicitly. If you snapshot exact request JSON in tests, the new key will only appear when you set it, so fixtures stay green.YooKassa\Common\Exceptions\ApiException subclasses (BadRequestException, NotFoundException, ForbiddenException, UnauthorizedException, InternalServerErrorException) separately from ResponseProcessingException (network/timeout — safe to retry with the same idempotence key). Don't catch \Exception around the gateway and swallow it.CurlClient. Wrap calls with retry(3, fn () => ...) and make sure the idempotence key is generated outside the retried closure.$client->setLogger() sends full request/response bodies to your PSR-3 logger. Receipts contain customer contact data; scrub or use a dedicated channel with a short retention.Receipt (or with item sums that don't match the payment amount) is rejected. platform_fee_amount does not change the receipt total — the buyer still pays the full amount.Client::setApiClient() for a custom HTTP transport (useful for Guzzle/Http:: integration and per-request timeouts), LoggerInterface, and NotificationFactory if you subclass notifications.How can I help you explore Laravel packages today?