phpguzzle.org
enrudees
Stripe – Документация

Комиссии 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-потоках недоступна.

Нашли неточность на этой странице?

Сообщить об ошибке