Комиссии Stripe в PHP: расчёт fee, Tax API и сверка
Проценты Stripe указаны на stripe.com/pricing. Хардкодить их в код бессмысленно: ставки зависят от страны аккаунта, типа карты и продукта, и Stripe периодически их обновляет. Полезнее понимать, откуда брать реальную комиссию по конкретному платежу через API.
Ни PaymentIntent, ни Charge не содержат поля с суммой комиссии. Эта информация живёт на отдельном объекте BalanceTransaction. Статья разбирает, как его получить, что лежит внутри fee_details, как работает application_fee на Connect-платформах и зачем нужен tax_behavior на объекте Price. Всё с PHP-кодом, который не сломается при очередном изменении тарифов.
Для базовой настройки SDK смотрите интеграцию платежей, а обработку ошибок при работе с платежами разбирали в статье про ошибки.
Где в API хранится комиссия
BalanceTransaction описывает каждое движение средств на балансе аккаунта: зачисление, списание, возврат, корректировку. У объекта три ключевых поля: amount (поступившая сумма), fee (удержанная комиссия), net (что осталось на балансе).
$stripe = new \Stripe\StripeClient('sk_test_...');
$charge = $stripe->charges->retrieve('ch_1abc...', [
'expand' => ['balance_transaction'],
]);
$bt = $charge->balance_transaction;
echo $bt->amount; // 10000 (в минорных единицах, центы)
echo $bt->fee; // 320
echo $bt->net; // 9680
echo $bt->currency; // "usd"
Без expand поле balance_transaction возвращает строку вида txn_1abc.... Можно получить объект и отдельным запросом:
$bt = $stripe->balanceTransactions->retrieve('txn_1abc...');
Почему fee нет на Charge
Stripe разделяет платёжное событие (Charge, PaymentIntent) и его влияние на баланс (BalanceTransaction). Разделение не случайное: один charge может породить несколько balance transaction при частичных возвратах, диспутах или конвертации валют.
fee_details: из чего складывается комиссия
Поле fee на BalanceTransaction содержит итоговую сумму. Разбивка по компонентам лежит в массиве fee_details:
foreach ($bt->fee_details as $detail) {
printf(
"%s: %d %s (%s)\n",
$detail->type,
$detail->amount,
$detail->currency,
$detail->description
);
}
Для внутренней карты результат будет одна строка:
stripe_fee: 59 usd (Stripe processing fees)
Международная карта с конвертацией валюты добавляет ещё строки:
stripe_fee: 59 usd (Stripe processing fees)
stripe_fee: 30 usd (International card fee)
stripe_fee: 20 usd (Currency conversion fee)
Возможные значения type: stripe_fee, application_fee, payment_method_passthrough_fee, tax, withheld_tax. У каждой строки свои amount и description.
Cross-border: три строки вместо одной
На Reddit периодически появляются темы про “Stripe взял 22% комиссии”. Обычно это результат наложения трёх строк: базовая обработка, международная карта, конвертация валюты. Если логировать только $bt->fee, непонятно, откуда взялась цифра. В продакшне полезно писать полную разбивку:
$feeLog = array_map(
fn($d) => "{$d->description}: {$d->amount}",
$bt->fee_details
);
error_log('Fee breakdown: ' . implode(', ', $feeLog));
Когда balance_transaction ещё не готов
BalanceTransaction создаётся, когда средства фактически перемещаются. В обработчике вебхука payment_intent.succeeded поле balance_transaction на charge может быть null, если Stripe ещё не создал транзакцию.
// Внутри обработчика вебхука
$intent = $event->data->object;
$charge = $stripe->charges->retrieve($intent->latest_charge, [
'expand' => ['balance_transaction'],
]);
if (null === $charge->balance_transaction) {
// Ещё не рассчитано, ретрай позже или ждём charge.updated
return;
}
$fee = $charge->balance_transaction->fee;
Для получения окончательных цифр подписывайтесь на charge.updated или invoice.finalized. К моменту payout.paid все balance transaction за период выплаты уже содержат финальные значения.
Функция получения комиссии
Вместо калькулятора с захардкоженными “2.9% + $0.30” запрашивайте реальные данные:
function getPaymentFee(\Stripe\StripeClient $stripe, string $chargeId): array
{
$charge = $stripe->charges->retrieve($chargeId, [
'expand' => ['balance_transaction'],
]);
$bt = $charge->balance_transaction;
if (null === $bt || !is_object($bt)) {
throw new \RuntimeException('Balance transaction not available yet');
}
$details = [];
foreach ($bt->fee_details as $d) {
$details[] = [
'type' => $d->type,
'amount' => $d->amount,
'currency' => $d->currency,
'description' => $d->description,
];
}
return [
'gross' => $bt->amount,
'fee' => $bt->fee,
'net' => $bt->net,
'details' => $details,
];
}
Функция возвращает актуальные данные вне зависимости от текущих ставок. Когда Stripe изменит тарифы, код продолжит работать.
application_fee для Connect-платформ
Маркетплейсы и SaaS-платформы на Stripe Connect собирают свою долю через параметр application_fee_amount. Это комиссия платформы, а не процессинговая комиссия Stripe.
$intent = $stripe->paymentIntents->create([
'amount' => 5000,
'currency' => 'usd',
'payment_method' => $pmId,
'confirm' => true,
'application_fee_amount' => 500, // $5.00 – доля платформы
], [
'stripe_account' => 'acct_connected_123',
]);
В fee_details connected-аккаунта появятся две строки:
stripe_fee: 175 usd (Stripe processing fees)
application_fee: 500 usd (Application fee)
Connected-аккаунт оплачивает обе. Платформа получает 500 центов как отдельную BalanceTransaction типа application_fee на своём аккаунте.
Получение application fees
$fees = $stripe->applicationFees->all([
'charge' => 'ch_1abc...',
'limit' => 10,
]);
foreach ($fees->data as $appFee) {
echo $appFee->amount; // 500
echo $appFee->account; // "acct_connected_123"
echo $appFee->balance_transaction; // BT на аккаунте платформы
}
Частая ошибка: путать application_fee_amount с процессинговой комиссией Stripe. Application fee идёт платформе, processing fee идёт Stripe. Они суммируются: с платежа $50.00 при application fee $5.00 connected-аккаунт получит примерно $43.25 после вычета обеих комиссий.
Расчёт налогов через Stripe Tax API
Stripe Tax (доступен с 2022 года) автоматизирует расчёт sales tax, VAT и GST. До его появления налоги добавлялись вручную как кастомные line items с захардкоженным процентом, что ломалось при изменении ставок. Для пользовательского платёжного потока (не Checkout) налог рассчитывается на сервере перед созданием PaymentIntent:
$calculation = $stripe->tax->calculations->create([
'currency' => 'usd',
'line_items' => [
[
'amount' => 10000, // $100 – цена товара
'reference' => 'L1', // внутренний ID
],
],
'customer_details' => [
'address' => [
'line1' => '920 5th Ave',
'city' => 'Seattle',
'state' => 'WA',
'postal_code' => '98104',
'country' => 'US',
],
'address_source' => 'shipping',
],
]);
echo $calculation->amount_total; // 11030 (товар + налог)
echo $calculation->tax_amount_exclusive; // 1030
// Используем amount_total как сумму PaymentIntent
$intent = $stripe->paymentIntents->create([
'amount' => $calculation->amount_total,
'currency' => 'usd',
'payment_method' => $pmId,
'confirm' => true,
]);
После оплаты фиксируем транзакцию для отчётности:
$stripe->tax->transactions->createFromCalculation([
'calculation' => $calculation->id,
'reference' => 'order_' . $orderId,
]);
tax_behavior: inclusive и exclusive
При создании объекта Price параметр tax_behavior определяет, включён ли налог в сумму:
$price = $stripe->prices->create([
'unit_amount' => 10000,
'currency' => 'eur',
'product' => 'prod_abc...',
'tax_behavior' => 'inclusive', // или 'exclusive'
]);
Exclusive (типично для США): налог начисляется сверху. Товар за $100.00 при ставке 10.3% обойдётся покупателю в $110.30.
Inclusive (типично для ЕС): указанная цена уже содержит налог. Товар за 100.00 EUR так и стоит 100.00 EUR; Stripe вычисляет долю налога обратным расчётом (например, 16.67 EUR при ставке НДС 20%).
В одной Checkout Session можно комбинировать оба режима. У каждого line item свой tax_behavior, Stripe корректно суммирует итог.
Объект Price и adaptive pricing
Объект Price пришёл на замену устаревшему Plan. Каждый продукт имеет хотя бы один Price с валютой, суммой и, для подписок, интервалом оплаты.
// Разовый платёж
$price = $stripe->prices->create([
'unit_amount' => 2500,
'currency' => 'usd',
'product' => 'prod_abc...',
]);
// Подписка (ежемесячно)
$monthly = $stripe->prices->create([
'unit_amount' => 1999,
'currency' => 'usd',
'product' => 'prod_abc...',
'recurring' => ['interval' => 'month'],
]);
Мультивалютность
Классический подход: создать по Price на каждую валюту.
$priceEur = $stripe->prices->create([
'unit_amount' => 2300,
'currency' => 'eur',
'product' => 'prod_abc...',
]);
Современная альтернатива: Adaptive Pricing. При включении на Checkout Session Stripe конвертирует базовую цену в локальную валюту покупателя по актуальному курсу. Один Price, всё остальное делает Stripe.
$session = $stripe->checkout->sessions->create([
'mode' => 'payment',
'line_items' => [[
'price' => 'price_abc...',
'quantity' => 1,
]],
'adaptive_pricing' => ['enabled' => true],
'success_url' => 'https://example.com/thanks',
'cancel_url' => 'https://example.com/cart',
]);
Покупатель видит цену в своей валюте на странице Checkout. Расчёт с продавцом происходит в валюте аккаунта. Комиссия за конвертацию появится отдельной строкой в fee_details.
Adaptive Pricing работает только с Checkout Sessions. В кастомном PaymentIntent-потоке мультивалютность по-прежнему требует отдельных Price или ручной конвертации.
Перенос комиссии на покупателя (surcharging)
Некоторые бизнесы добавляют к сумме надбавку, покрывающую комиссию Stripe. Формула выглядит простой, но большинство реализаций считают неправильно:
// Неправильно: надбавка сама облагается комиссией
$amount = 10000;
$surcharge = (int) round($amount * 0.029 + 30);
$total = $amount + $surcharge;
// Stripe возьмёт 2.9% + $0.30 от $total, а это больше, чем $surcharge
// Правильно: решаем уравнение total - fee(total) = amount
// total = (amount + fixed) / (1 - rate)
$rate = 0.029;
$fixed = 30; // центов
$total = (int) ceil(($amount + $fixed) / (1 - $rate));
// $total = 10330, fee от 10330 = 330, net = 10000 = $100
Юридический момент: surcharging кредитных карт регулируется или запрещён в ряде юрисдикций (ЕС, Австралия, отдельные штаты США). Проверяйте местное законодательство перед внедрением.
Микроплатежи: когда фиксированная часть перевешивает
На мелких суммах фиксированная часть комиссии ($0.30) занимает непропорционально большую долю. Эффективная ставка на $2.00 может удивить:
function effectiveRate(int $amountCents, float $rate, int $fixedCents): float
{
$fee = (int) round($amountCents * $rate) + $fixedCents;
return round($fee / $amountCents * 100, 2);
}
echo effectiveRate(200, 0.029, 30); // 18.0% на $2.00
echo effectiveRate(500, 0.029, 30); // 9.0% на $5.00
echo effectiveRate(10000, 0.029, 30); // 3.2% на $100.00
echo effectiveRate(50000, 0.029, 30); // 2.96% на $500.00
Для товаров дешевле $5.00 имеет смысл объединять несколько позиций в одну транзакцию или устанавливать минимальную сумму заказа. Stripe раньше предлагал тариф для микроплатежей с пониженной фиксированной частью, но для новых аккаунтов он недоступен. Актуальные ставки на stripe.com/pricing.
Возвраты: Stripe оставляет комиссию себе
С сентября 2017 для новых аккаунтов (и с сентября 2020 для legacy) Stripe не возвращает процессинговую комиссию при refund. Полный возврат платежа $100.00 обходится продавцу в сумму исходной комиссии.
$refund = $stripe->refunds->create([
'charge' => 'ch_1abc...',
]);
// Balance transaction возврата
$refundBt = $stripe->balanceTransactions->retrieve(
$refund->balance_transaction
);
echo $refundBt->amount; // -10000 (возврат покупателю)
echo $refundBt->fee; // 0 (комиссию не возвращают)
echo $refundBt->net; // -10000 (баланс уменьшается на всю сумму)
Исходная комиссия (например, 320 центов) остаётся у Stripe. Реальный убыток продавца при полном возврате: сумма_платежа + исходная_комиссия. Учитывайте это при расчёте финансовых показателей и в коде сверки.
Старые калькуляторы и блоги, которые предполагают возврат комиссии, дадут неправильные цифры. Если ваш код считает стоимость возвратов, он не должен вычитать исходную комиссию из суммы возврата.
Stripe, PayPal, Square: сравнение для разработчика
Базовые ставки всех трёх систем различаются на десятые доли процента. Текущие цифры на stripe.com/pricing, paypal.com/webapps/mpp/merchant-fees, squareup.com/pricing. Реальная разница для PHP-разработчика не в процентах, а в API.
Прозрачность fee в API. Stripe отдаёт каждый компонент через balance_transaction.fee_details. PayPal возвращает transaction_fee одной цифрой, без разбивки на cross-border, конвертацию и тип карты. Square ближе к Stripe: массив processing_fee на объекте Payment содержит отдельные строки.
Политика возвратов. Stripe и PayPal (с 2019) оставляют комиссию при refund. Суммы различаются, но модель одинаковая.
Удержание комиссии. Stripe вычитает fee из каждого платежа до зачисления на баланс; поле net показывает итог. PayPal списывает fee с баланса PayPal-аккаунта, итог нужно считать из gross_amount - fee_amount.
Маркетплейсы. Stripe Connect даёт application_fee_amount с явной маршрутизацией на платформу. PayPal (Partner Referrals / Marketplace) использует другую модель согласования fee, менее прозрачную в ответе API.
Критерий выбора для разработчика: какой API предоставляет надёжные данные о fee без ручных расчётов. По гранулярности fee_details Stripe выигрывает.
Сверка: проверка данных через API
Для финансовой отчётности можно итерировать balance transactions за период и проверить итоги:
$transactions = $stripe->balanceTransactions->all([
'created' => [
'gte' => strtotime('2026-04-01'),
'lte' => strtotime('2026-04-30'),
],
'type' => 'charge',
'limit' => 100,
]);
$totalGross = 0;
$totalFee = 0;
$totalNet = 0;
foreach ($transactions->autoPagingIterator() as $bt) {
$totalGross += $bt->amount;
$totalFee += $bt->fee;
$totalNet += $bt->net;
}
// Контроль: net должен равняться gross минус fees
assert($totalNet === $totalGross - $totalFee);
printf(
"Gross: %s, Fees: %s, Net: %s\n",
number_format($totalGross / 100, 2),
number_format($totalFee / 100, 2),
number_format($totalNet / 100, 2)
);
Метод autoPagingIterator() обрабатывает пагинацию автоматически через starting_after. Подробнее о пагинации в справочнике по API.
Фильтр type отделяет charges от payouts, refunds и корректировок. Для полного отчёта по всем типам транзакций группируйте результаты по полю reporting_category на каждой возвращённой транзакции: оно соответствует категориям финансовых отчётов Stripe и упрощает сверку с экспортом из Dashboard.
FAQ
Stripe берёт 3%?
Базовая ставка зависит от страны аккаунта и типа карты. Для US-аккаунтов обычно 2.9% + $0.30 за успешный платёж, но для международных карт, AMEX и отдельных методов оплаты цифры другие. Актуальные ставки на stripe.com/pricing. В коде не хардкодьте процент; читайте balance_transaction.fee после проведения платежа.
Сколько возьмёт Stripe с $100?
Для внутренней US-карты примерно $3.20 (2.9% от $100 + $0.30). При международной карте добавляется cross-border surcharge. Если была конвертация валюты, прибавляется и она. Единственный источник точных данных: balance_transaction.fee на конкретном платеже. Внутренний и международный платёж одинаковой суммы дадут разную комиссию.
Подключение Stripe бесплатно?
Нет платы за регистрацию, ежемесячной подписки или минимального объёма. Оплата только за транзакции. API, Dashboard, тестовый режим и инструменты разработчика бесплатны.
Как добавить надбавку 3%?
Формула: total = (amount + fixed_fee) / (1 - rate). Наивный расчёт amount * 0.03 занижает надбавку, потому что с неё самой тоже удерживается комиссия. Подробности в секции surcharging. Перед внедрением проверьте, разрешён ли surcharging в вашей юрисдикции.
PayPal или Stripe дешевле?
Базовые ставки различаются на 0.1-0.5% в зависимости от региона. Реальная разница определяется политикой возвратов, cross-border fee и скидками за объём. Оба сервиса оставляют комиссию при refund. Для бизнеса с большим объёмом Stripe предлагает индивидуальные ставки. Сравнивайте на основе реального микса транзакций (внутренние vs международные, средний чек, процент возвратов), а не заголовочных процентов.
Stripe берёт комиссию с дебетовых карт?
Да. В большинстве регионов ставка на дебетовые карты совпадает со ставкой на кредитные. В некоторых странах дебетовые ставки ниже. Актуальные тарифы для вашего аккаунта можно посмотреть в Dashboard: Settings > Payment methods.
Что такое adaptive pricing?
Функция Checkout, которая автоматически конвертирует цену товара в локальную валюту покупателя по актуальному курсу. Вы задаёте один Price в базовой валюте, Stripe показывает покупателю цену в его валюте. Расчёт с продавцом в валюте аккаунта. Комиссия за конвертацию отражается в fee_details. Работает только в Checkout Sessions, в кастомных PaymentIntent-потоках недоступна.
Нашли неточность на этой странице?
Сообщить об ошибке