Приём платежей в Laravel: сравниваем Cashier, SDK и API
Три способа подключить платежи к Laravel-приложению: пакет Cashier для подписок, официальные SDK шлюзов и прямой вызов API. Разбираем цену каждого подхода и когда что брать.

Задача звучит просто: пользователь нажимает «Оплатить», деньги приходят на счёт, заказ помечается как оплаченный. На практике вы упираетесь в выбор: тянуть готовый пакет вроде Laravel Cashier, ставить официальный SDK платёжного шлюза или общаться с его REST API напрямую. Каждый вариант выигрывает в одном и проигрывает в другом — и цена ошибки здесь не гипотетическая, а в реальных деньгах и в разборе, почему заказ оплачен, но не отгружен.
Разбираем три подхода на актуальной связке Laravel 11.x и PHP 8.2+. Речь про интеграцию как таковую — выбор эквайрера, тарифы и юридическую сторону приёма платежей здесь не рассматриваем.
Три способа подключить оплату
Прежде чем писать код, поймите, что именно вы интегрируете. Разовый платёж за заказ, регулярную подписку и маркетплейс с выплатами продавцам подключают по-разному, и «универсального» решения нет.
Вариант 1. Laravel Cashier
Cashier — официальный пакет для подписок и разовых списаний. Есть две отдельные реализации: laravel/cashier (Stripe) и laravel/cashier-paddle (Paddle). Это не универсальная обёртка над любым шлюзом, а именно интеграция с конкретным провайдером, и это первое, что упускают.
Cashier берёт на себя модель подписки, пробные периоды, смену тарифа, инвойсы и вебхуки. Вы получаете методы прямо на модели пользователя:
// composer require laravel/cashier
// В модели User подключён трейт Billable
use App\Models\User;
$user = User::find(1);
// Разовое списание на 999 центов (валюта задаётся в конфиге)
$user->charge(999, $paymentMethodId);
// Оформление подписки на тариф
$user->newSubscription('default', 'price_id_из_stripe')
->create($paymentMethodId);
// Проверка активной подписки
if ($user->subscribed('default')) {
// доступ открыт
}Что выигрываете: подписки, инвойсы и обработку большинства сценариев Stripe/Paddle не пишете руками. Чем платите: вы привязаны к одному из двух провайдеров. Если завтра нужно добавить российский шлюз или второй эквайрер, Cashier не поможет — придётся ставить отдельную интеграцию рядом.
Вариант 2. Официальный SDK шлюза
У большинства платёжных провайдеров есть PHP-SDK, который ставится через Composer и оборачивает их API в классы. Для разовых платежей без подписочной логики это чаще всего оптимальный баланс: меньше кода, чем у прямого API, и никакой привязки к чужой модели подписок.
Конкретные имена классов и методов зависят от провайдера и версии SDK — их берите из документации вашего шлюза, а не по памяти. Общая схема одинакова: создаёте объект клиента с ключами, формируете платёж, получаете ссылку на страницу оплаты или токен для формы, ловите результат в вебхуке.
SDK экономит время ровно до того момента, пока делает то, что задумали его авторы. Как только вам нужен нестандартный параметр запроса, которого нет в методах обёртки, вы возвращаетесь к прямому API — но уже через слой чужих абстракций.
Вариант 3. Прямой вызов API через HTTP-клиент
Laravel несёт встроенный HTTP-клиент на базе Guzzle (Illuminate\Support\Facades\Http). Ничего дополнительно ставить не нужно — вы дёргаете REST API шлюза напрямую.
use Illuminate\Support\Facades\Http;
// Создание платежа прямым запросом к API шлюза.
// URL, поля тела и заголовки — строго из документации провайдера.
$response = Http::withToken(config('services.gateway.secret'))
->acceptJson()
->post('https://api.provider.example/v1/payments', [
'amount' => ['value' => '1000.00', 'currency' => 'RUB'],
'capture' => true,
'confirmation' => [
'type' => 'redirect',
'return_url' => route('payment.callback'),
],
'description' => 'Заказ №'.$order->id,
]);
if ($response->failed()) {
// логируем тело и статус, не глотаем ошибку молча
logger()->error('Payment API error', [
'status' => $response->status(),
'body' => $response->body(),
]);
abort(502);
}
$paymentId = $response->json('id');
$confirmUrl = $response->json('confirmation.confirmation_url');Что выигрываете: полный контроль, любой параметр, любой шлюз, никаких лишних зависимостей. Чем платите: вебхуки, идемпотентность, повторы при таймаутах, парсинг статусов — всё это вы пишете и поддерживаете сами.
Сравнение по делу
| Критерий | Cashier | SDK шлюза | Прямой API |
|---|---|---|---|
| Провайдеры | Только Stripe / Paddle | Один, у которого есть SDK | Любой с REST API |
| Подписки из коробки | Да | Частично | Пишете сами |
| Разовый платёж | Просто | Просто | Средне |
| Обработка вебхуков | Готовый контроллер | Часто есть хелперы | Полностью на вас |
| Смена провайдера | Болезненно | Переписать слой | Переписать слой |
| Лишние зависимости | Пакет + SDK Stripe | SDK провайдера | Нет (Guzzle уже есть) |
Когда что выбирать
- Берите Cashier, если продаёте подписки и уже выбрали Stripe или Paddle. Писать подписочную логику руками, когда за вас это сделал официальный пакет, — трата недель на то, что даёт баги на пробных периодах и пропорциональных доплатах.
- Берите SDK, если у вас разовые платежи через один шлюз с готовым официальным SDK, а сложных сценариев не предвидится. Меньше кода, чем прямой API, при почти той же гибкости.
- Берите прямой API, если шлюз без вменяемого SDK, если вам нужны два провайдера сразу или если вы строите свой слой абстракции над оплатой и не хотите тянуть чужие модели данных.
Практичный компромисс для растущего проекта: спрятать любой из вариантов за собственным интерфейсом PaymentGateway с методами вроде createPayment() и handleWebhook(). Тогда замена реализации не расползётся по контроллерам.
Что ломается в проде
Независимо от подхода, платёжная интеграция ломается в одних и тех же местах — и почти всегда на вебхуках.
- Вебхук не проверяется на подпись. Любой, кто узнал ваш URL, шлёт «оплачено» и получает товар бесплатно. Каждый шлюз даёт способ верифицировать подпись запроса — используйте его, иначе эндпоинт открыт.
- CSRF-защита режет вебхук. Входящий POST от платёжной системы не несёт вашего CSRF-токена. Маршрут вебхука выносите из группы
webс CSRF-проверкой или добавляйте его в исключения — иначе Laravel вернёт 419, а платёж повиснет. - Нет идемпотентности. Шлюз повторяет вебхук при таймауте. Если вы не проверяете, обработан ли уже этот платёж по его ID, заказ отгрузится дважды.
- Статус берётся из редиректа, а не из вебхука. Пользователь может закрыть вкладку до возврата. Источник истины об оплате — вебхук или явный запрос статуса к API, а не то, на какую страницу вернулся браузер.
- Секретные ключи в коде. Ключи живут в
.envи читаются черезconfig(), а не хардкодятся. В примерах и репозитории — только заглушки.
Что делать дальше
Определитесь с провайдером и сценарием (разовый платёж или подписка) — это отсекает половину вариантов сразу. Затем поднимите тестовый эндпоинт вебхука и прогоните через него платёж в песочнице шлюза: именно тут вылезает большинство проблем, а не на создании платежа. Проверьте, что повторный вебхук с тем же ID не создаёт второй заказ, и залогируйте все входящие запросы от шлюза на первые недели работы — разбор спорных платежей без логов превращается в гадание.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: Laravel Cashier (Stripe) — официальная документация, Laravel HTTP Client — официальная документация
Частые вопросы
Cashier работает только со Stripe?
У меня Laravel 10, а не 11 — примеры подойдут?
Что делать, если вебхук приходит, а заказ не помечается оплаченным?
Нужно ли ставить Guzzle для прямых запросов к API?
Можно ли подключить два платёжных шлюза одновременно?
Как безопасно тестировать оплату без реальных денег?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.