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

Stripe PHP SDK (stripe/stripe-php) оборачивает REST API Stripe в типизированные PHP-классы: аутентификация, сериализация, retry на транзиентных ошибках и десериализация ответов в объекты, к которым можно обращаться через ->. Официальная документация описывает каждый метод отдельно, но не объясняет практические вещи: когда делать expand вложенного объекта, а когда запрашивать отдельно; как ведёт себя пагинация на больших выборках; что скрывается внутри объекта ответа. Об этом здесь.

Если SDK ещё не установлен, гайд по интеграции платежей покрывает Composer, API-ключи и первый PaymentIntent. Всё ниже предполагает, что SDK подключён.

StripeClient vs Stripe::setApiKey

В SDK два способа вызывать API. Смешивать их в одном проекте не стоит.

Глобальный статический (legacy):

\Stripe\Stripe::setApiKey('sk_test_...');

$customer = \Stripe\Customer::create(['email' => '[email protected]']);
$intent   = \Stripe\PaymentIntent::retrieve('pi_abc123');

Через экземпляр клиента (рекомендуется с v7.33):

$stripe = new \Stripe\StripeClient('sk_test_...');

$customer = $stripe->customers->create(['email' => '[email protected]']);
$intent   = $stripe->paymentIntents->retrieve('pi_abc123');

Разница не в синтаксисе. setApiKey сохраняет ключ в статическом свойстве. Любой код в том же процессе использует тот же ключ: тесты, очереди, воркеры для разных Stripe-аккаунтов. Один воркер перезаписал ключ, следующий отправил запрос с чужими credentials и получил “No such payment_intent” без подсказки, что виноват ключ.

StripeClient хранит ключ на экземпляре. Два экземпляра с разными ключами спокойно живут в одном процессе. Для Laravel queue worker, обрабатывающего задачи Connect-аккаунтов, это единственный способ избежать утечки данных между аккаунтами.

// Connect: работа от лица подключённого аккаунта
$stripe = new \Stripe\StripeClient([
    'api_key'        => 'sk_test_platform_key',
    'stripe_account' => 'acct_connected_123',
]);

Для нового кода всегда используйте StripeClient. Статический паттерн работает, дату удаления Stripe не объявлял, но новые endpoint-ы API поддерживаются только через сервисные методы на клиенте. Миграция с legacy занимает минуты: создать экземпляр клиента, заменить \Stripe\Customer::create( на $stripe->customers->create( по всему проекту. Массивы аргументов не меняются.

Аутентификация и API-ключи

Stripe выдаёт две пары ключей: для тестового и для живого режима.

ПрефиксНазначениеГде используется
sk_test_Секретный ключ, тестТолько сервер, env-переменная
sk_live_Секретный ключ, productionТолько сервер, env-переменная
pk_test_Публичный ключ, тестБраузер (Stripe.js)
pk_live_Публичный ключ, productionБраузер (Stripe.js)

Секретные ключи аутентифицируют все серверные вызовы. Публичные безопасно отдавать в клиентский JavaScript: через них можно только подтверждать платежи и создавать токены, но не читать или записывать данные аккаунта.

Истекают ли API-ключи Stripe? Нет. Ключи живут, пока вы не откатите (roll) их вручную. Rollover генерирует новый ключ с grace-окном (24 часа для секретных ключей, настраивается), в течение которого работают и старый, и новый. После окна старый ключ перестаёт принимать запросы. Управление ключами доступно в Dashboard: Developers > API keys.

Restricted keys – третий тип: вы создаёте их в Dashboard с конкретными правами (читать charges, писать customers, больше ничего). Подходят для микросервисов и интеграций с третьими сторонами, где полный секретный ключ – слишком широкий доступ.

Ключи вне кода

Правило одно: ключи в переменных окружения, не в PHP-файлах. Утечка sk_live_ даёт полный доступ к Stripe-аккаунту.

// Правильно: из env при старте
$stripe = new \Stripe\StripeClient(getenv('STRIPE_SECRET_KEY'));

// Плохо: захардкожено, попадёт в git
$stripe = new \Stripe\StripeClient('sk_live_actualKeyHere');

В Laravel ключи хранятся в .env и вытаскиваются через config/services.php:

// config/services.php
'stripe' => [
    'secret' => env('STRIPE_SECRET_KEY'),
],

// в любом месте приложения
$stripe = new \Stripe\StripeClient(config('services.stripe.secret'));

Полный Service Provider с singleton-биндингом и подменой в тестах описан в гайде по Laravel.

Паттерн вызовов

Все ресурсы SDK устроены одинаково. Методы соответствуют HTTP-глаголам:

Метод SDKHTTPПример
->create([...])POSTСоздать Customer
->retrieve('id')GETПолучить PaymentIntent
->update('id', [...])POSTОбновить Subscription
->delete('id')DELETEУдалить купон
->all([...])GETСписок charges

Первый аргумент retrieve, update и delete – всегда ID объекта строкой. create и all принимают ассоциативный массив параметров. Эта сигнатура единообразна для всех ресурсов:

$stripe = new \Stripe\StripeClient('sk_test_...');

// Создание
$customer = $stripe->customers->create([
    'email' => '[email protected]',
    'name'  => 'Jane Doe',
    'metadata' => ['internal_id' => '42'],
]);

// Получение
$customer = $stripe->customers->retrieve('cus_abc123');

// Обновление
$stripe->customers->update('cus_abc123', [
    'name' => 'Jane Smith',
]);

// Удаление
$stripe->customers->delete('cus_abc123');

// Список
$customers = $stripe->customers->all(['limit' => 10]);

Дополнительные параметры

retrieve принимает ID и опциональный массив параметров API (например, expand):

$intent = $stripe->paymentIntents->retrieve(
    'pi_abc123',
    ['expand' => ['customer', 'payment_method']]
);

update принимает ID, массив изменяемых полей, и опционально третий массив для request-level опций (idempotency key, Stripe-Account header). expand – обычный параметр API, он идёт в массив данных:

$stripe->paymentIntents->update('pi_abc123', [
    'metadata' => ['order_id' => '99'],
    'expand'   => ['customer'],
]);

// Request-level опции идут в третий аргумент
$stripe->paymentIntents->update(
    'pi_abc123',
    ['metadata' => ['order_id' => '99']],
    ['idempotency_key' => 'update_pi_abc123_v2']
);

Expand: раскрытие вложенных объектов

По умолчанию API возвращает связанные объекты как голые ID. Поле customer в PaymentIntent придёт как строка "cus_abc123", а не полный объект Customer. Параметр expand указывает Stripe подставить полный объект прямо в ответ, чтобы не делать второй запрос.

$intent = $stripe->paymentIntents->retrieve('pi_abc123', [
    'expand' => ['customer', 'payment_method'],
]);

// Теперь это полные объекты, не строки
echo $intent->customer->email;
echo $intent->payment_method->card->last4;

Ограничения

Максимальная глубина вложенности – четыре уровня, и не больше 10 expand-ов на запрос. Путь через точку разворачивает цепочку:

$charge = $stripe->charges->retrieve('ch_abc', [
    'expand' => ['payment_intent.customer.default_source'],
]);

Здесь разворачивается PaymentIntent внутри Charge, Customer внутри PaymentIntent, и default source на Customer. Глубже четырёх уровней или больше 10 полей в одном запросе – ошибка.

Expand в списках

При запросе списка к путям expand-а добавляется префикс data.:

$charges = $stripe->charges->all([
    'limit' => 50,
    'expand' => ['data.customer', 'data.balance_transaction'],
]);

foreach ($charges->data as $charge) {
    echo $charge->customer->email;
    echo $charge->balance_transaction->fee;
}

Каждый expand в списке умножает объём ответа и время. Stripe внутренне запрашивает каждый развёрнутый объект. Если из связанного объекта нужно одно поле на 100 записей, стоит прикинуть, не быстрее ли отдельный целевой запрос.

Expand при создании

Expand работает и на create. Полезно, когда нужно сразу получить автоматически созданный вложенный объект:

$session = $stripe->checkout->sessions->create([
    'mode' => 'payment',
    'line_items' => [['price' => 'price_abc', 'quantity' => 1]],
    'success_url' => 'https://example.com/thanks',
    'expand' => ['payment_intent'],
]);

// payment_intent – полный объект, не строка pi_xxx
echo $session->payment_intent->status;

Пагинация

Списочные endpoint-ы отдают максимум 100 объектов за вызов (по умолчанию 10). Stripe использует курсорную пагинацию: передаёте ID последнего полученного объекта, API возвращает следующую страницу.

Ручная пагинация

$hasMore = true;
$startingAfter = null;

while ($hasMore) {
    $params = ['limit' => 100];
    if (null !== $startingAfter) {
        $params['starting_after'] = $startingAfter;
    }

    $charges = $stripe->charges->all($params);

    foreach ($charges->data as $charge) {
        processCharge($charge);
    }

    $hasMore = $charges->has_more;
    if ($charges->data) {
        $startingAfter = end($charges->data)->id;
    }
}

Автопагинация

SDK предоставляет autoPagingIterator(), который берёт логику курсора на себя:

$charges = $stripe->charges->all(['limit' => 100]);

foreach ($charges->autoPagingIterator() as $charge) {
    // Итерирует через ВСЕ charges, подгружая новые страницы автоматически
    processCharge($charge);
}

autoPagingIterator делает новый API-вызов, когда текущая страница заканчивается. Параметр limit контролирует размер страницы, а не общее количество. С limit => 100 каждый HTTP-запрос забирает 100 объектов; итератор продолжает, пока has_more не станет false.

Встроенного способа остановиться после N объектов нет. Если нужны первые 500 charges, поставьте счётчик:

$count = 0;
foreach ($charges->autoPagingIterator() as $charge) {
    processCharge($charge);
    if (++$count >= 500) break;
}

Фильтрация списков

Большинство списочных endpoint-ов принимают фильтры, сужающие выборку на стороне сервера. Фильтровать до пагинации всегда лучше, чем тянуть всё и фильтровать в PHP:

// Charges за последние 7 дней
$charges = $stripe->charges->all([
    'limit'   => 100,
    'created' => ['gte' => strtotime('-7 days')],
]);

// Подписки конкретного клиента
$subs = $stripe->subscriptions->all([
    'customer' => 'cus_abc123',
    'status'   => 'active',
    'limit'    => 100,
]);

Фильтр created принимает ключи gt, gte, lt, lte с Unix-таймстампами.

Объекты ответов

Каждый ответ API приходит как StripeObject (или подкласс: Customer, PaymentIntent и т.д.). Эти объекты ведут себя как гибрид: к полям можно обращаться и через свойства ($customer->email), и через ключи массива ($customer['email']), но это ни обычный объект, ни обычный массив.

Конвертация в JSON

json_encode($stripeObject) работает: объект реализует JsonSerializable и возвращает данные ответа. Для явной конвертации:

$customer = $stripe->customers->retrieve('cus_abc123');

// Работает: сериализует поля данных объекта
$json = json_encode($customer);

// Надёжнее: сперва toArray(), потом encode
$array = $customer->toArray();
$json  = json_encode($array, JSON_PRETTY_PRINT);

toArray() возвращает чистый ассоциативный массив без внутренностей SDK. Для сохранения ответа Stripe в базу или кеш используйте toArray() и json_encode на результат.

Проверка null-полей

Stripe возвращает null для незаданных полей. SDK это сохраняет, поэтому $customer->phone будет null, а не undefined. Используйте null coalescing:

$phone = $customer->phone ?? 'телефон не указан';

Свойство lastResponse

Каждый StripeObject содержит lastResponse – сырой HTTP-ответ от API-вызова:

$customer = $stripe->customers->retrieve('cus_abc123');

$httpStatus = $customer->lastResponse->code;      // 200
$requestId  = $customer->lastResponse->headers['Request-Id'];
$rawBody    = $customer->lastResponse->body;       // JSON-строка

Request-Id нужен при обращении в Stripe support. Логируйте его при каждом API-вызове в production – это первое, что у вас спросят в тикете.

Тестовые карты и токены

Тестовый режим – полноценная песочница. Деньги не списываются, карточные сети не участвуют. Но тестовые объекты – реальные API-объекты с реальными ID, и все функции работают так же, как в production: webhooks, 3DS, диспуты, возвраты.

Номера карт для Stripe.js / Elements

Когда фронтенд собирает данные карты через Stripe.js:

НомерБрендПоведение
4242 4242 4242 4242VisaУспех
5555 5555 5555 4444MastercardУспех
3782 822463 10005AmexУспех
4000 0025 0000 3155VisaТребует 3DS-аутентификации
4000 0000 0000 9995VisaОтклонена: insufficient_funds
4000 0000 0000 0002VisaОтклонена: generic_decline
4000 0000 0000 0069VisaОтклонена: expired_card
4000 0000 0000 0127VisaОтклонена: incorrect_cvc

Дата истечения – любая будущая, CVC – любые 3 цифры (4 для Amex).

Серверные тестовые токены

Для тестирования серверной логики без фронтенда можно подставлять готовые тестовые PaymentMethod:

$stripe = new \Stripe\StripeClient('sk_test_...');

$intent = $stripe->paymentIntents->create([
    'amount'         => 2000,
    'currency'       => 'usd',
    'payment_method' => 'pm_card_visa',
    'confirm'        => true,
    'automatic_payment_methods' => [
        'enabled'         => true,
        'allow_redirects' => 'never',
    ],
]);

echo $intent->status; // "succeeded"

Токены pm_card_* пропускают шаг со Stripe.js. Полезны для интеграционных тестов, проверки webhook-пайплайна и CI.

ТокенЧто симулирует
pm_card_visaУспешный платёж Visa
pm_card_mastercardУспешный платёж Mastercard
pm_card_visa_debitДебетовая Visa
pm_card_chargeDeclinedgeneric_decline
pm_card_chargeDeclinedInsufficientFundsinsufficient_funds
pm_card_chargeDeclinedExpiredCardexpired_card
pm_card_authenticationRequiredТребует 3DS

Устаревший формат tok_visa / tok_chargeDeclined до сих пор работает через внутреннюю конвертацию, но pm_card_* – актуальная рекомендация. Используйте pm_card_* для PaymentIntents; tok_* – только если приходится работать с legacy Charges API.

Полный справочник по decline-кодам и обработке каждого из них – в гайде по ошибкам.

Версионирование API

API Stripe развивается через датированные версии вида 2026-03-25.dahlia. Каждая мажорная версия может менять формат ответов, переименовывать поля или менять дефолтное поведение. Код привязан к конкретной версии, и Stripe гарантирует обратную совместимость внутри неё.

Как SDK пиннит версию

SDK пиннит версию API к той, которая была актуальной на момент сборки конкретного релиза. У аккаунта в Dashboard своя дефолтная версия. Запросы через SDK используют версию SDK, не Dashboard.

// Узнать версию, которую использует SDK
echo \Stripe\Stripe::getApiVersion();
// например: "2026-03-25.dahlia"

Переопределение версии

Можно задать конкретную версию на уровне клиента или отдельного запроса:

// На уровне клиента
$stripe = new \Stripe\StripeClient([
    'api_key'        => 'sk_test_...',
    'stripe_version' => '2024-12-18.acacia',
]);

// На уровне запроса (статический паттерн)
\Stripe\Stripe::setApiKey('sk_test_...');
$customer = \Stripe\Customer::create(
    ['email' => '[email protected]'],
    ['stripe_version' => '2024-12-18.acacia']
);

Переопределяйте осторожно. Один вызов на другой версии – и формат ответа отличается от остального кода. Хороший источник трудно отлавливаемых багов.

Webhooks и версии

Webhook-события приходят в формате API-версии, сконфигурированной для endpoint-а в Dashboard, а не в версии SDK. Если endpoint настроен на версию X, а SDK ожидает версию Y, структура события может не совпасть с тем, что ожидает код.

Держите их синхронизированными: после обновления SDK и проверки поднимите версию webhook endpoint-а в Dashboard. В гайде по webhooks там же про верификацию подписи.

Идемпотентность

Stripe поддерживает ключи идемпотентности на всех POST-запросах. Передайте уникальный ключ, и если тот же запрос сработает дважды (повтор из-за сети, двойной клик, повторная доставка webhook), Stripe вернёт прежний результат без создания дубликата.

$stripe = new \Stripe\StripeClient('sk_test_...');

$intent = $stripe->paymentIntents->create([
    'amount'   => 5000,
    'currency' => 'usd',
], [
    'idempotency_key' => 'order_42_payment_attempt_1',
]);

Ключ привязан к запросу. Stripe хранит результат 24 часа: повторный запрос с тем же ключом и теми же параметрами возвращает закэшированный ответ. Тот же ключ, но другие параметры – ошибка 400.

Хороший ключ: доменный идентификатор плюс намерение. order_{id}_payment_{attempt} работает. UUID тоже годится, но его сложнее отлаживать. Таймстамп один – плохо: два запроса в одну миллисекунду с одинаковым таймстампом создадут ту самую проблему, от которой идемпотентность должна защищать.

Где идемпотентность важнее всего

  • Создание PaymentIntent. Сеть оборвалась после того, как Stripe обработал запрос, но до того, как ваш сервер получил ответ. Без ключа повтор создаст второй платёж.
  • Возвраты. Та же ситуация. Двойной возврат хуже двойного списания: клиент не пожалуется, а вы не заметите до сверки.
  • Webhooks. Handler должен быть идемпотентным по дизайну (проверить, не помечен ли заказ уже оплаченным, прежде чем помечать). Ключ идемпотентности защищает исходящую сторону; идемпотентность handler-а – входящую.

Иерархия исключений

SDK бросает типизированные исключения на каждую ошибку API. Дерево классов:

\Stripe\Exception\ApiErrorException (абстрактный базовый)
├── CardException              // 402 – карта отклонена, insufficient_funds и т.д.
├── InvalidRequestException    // 400 – пропущены параметры, неверный ID, не тот режим
├── AuthenticationException    // 401 – неверный API-ключ
├── ApiConnectionException     // сетевая ошибка, таймаут, DNS
├── IdempotencyException       // конфликт ключа идемпотентности
├── PermissionException        // 403 – restricted key, недостаточно прав
├── RateLimitException         // 429 – слишком много запросов
├── UnknownApiErrorException   // всё остальное от API
└── SignatureVerificationException  // несовпадение подписи webhook (не подкласс)

SignatureVerificationException не наследует ApiErrorException, потому что возникает при локальной проверке подписи, а не при API-вызове. Если ловите ApiErrorException как catch-all, SignatureVerificationException проскользнёт мимо.

Порядок catch-блоков важен. CardException наследует ApiErrorException, поэтому если ApiErrorException стоит первым, блок CardException никогда не выполнится:

try {
    $intent = $stripe->paymentIntents->create([...]);
} catch (\Stripe\Exception\CardException $e) {
    // Должен стоять ДО ApiErrorException
    $decline = $e->getError()->decline_code;
    log("Карта отклонена: {$decline}");
} catch (\Stripe\Exception\RateLimitException $e) {
    // 429: отступить и повторить
    sleep(2);
} catch (\Stripe\Exception\InvalidRequestException $e) {
    // Неверные параметры, не тот режим, пропущены поля
    log("Неверный запрос: " . $e->getMessage());
} catch (\Stripe\Exception\ApiErrorException $e) {
    // Catch-all для остального от API
    log("Ошибка Stripe: " . $e->getMessage());
}

Полный разбор типов ошибок, decline-кодов и стратегий восстановления – в гайде по ошибкам.

Metadata

Каждый крупный объект Stripe принимает metadata: до 50 ключей, ключ до 40 символов, значение до 500 символов. Metadata – связующее звено между миром Stripe и вашим приложением.

$intent = $stripe->paymentIntents->create([
    'amount'   => 7500,
    'currency' => 'eur',
    'metadata' => [
        'order_id'    => '1042',
        'campaign'    => 'summer_sale',
        'internal_id' => 'usr_887',
    ],
]);

Metadata путешествует с объектом по всему жизненному циклу. Когда PaymentIntent завершится и прилетит webhook, $event->data->object->metadata->order_id вернёт "1042". Так webhook-handler понимает, какой заказ помечать оплаченным, без запросов к базе по сумме или времени.

Metadata не копируется между объектами

На этом спотыкаются регулярно. Metadata на Checkout Session не копируется автоматически в созданный ею PaymentIntent. Если webhook-handler слушает payment_intent.succeeded и читает metadata там, массив окажется пустым – если не заполнить payment_intent_data.metadata при создании Session:

$session = $stripe->checkout->sessions->create([
    'mode' => 'payment',
    'line_items' => [['price' => 'price_abc', 'quantity' => 1]],
    'success_url' => 'https://example.com/thanks',
    'metadata' => ['order_id' => '42'],               // живёт на Session
    'payment_intent_data' => [
        'metadata' => ['order_id' => '42'],            // живёт на PaymentIntent
    ],
]);

Или не дублировать, а слушать checkout.session.completed вместо payment_intent.succeeded. Подробнее – в гайде по Checkout.

Логирование и отладка

Request-Id

Каждый ответ API содержит заголовок Request-Id. Логируйте его:

$customer = $stripe->customers->create(['email' => '[email protected]']);
$requestId = $customer->lastResponse->headers['Request-Id'];
error_log("Stripe request: {$requestId}");

Когда в production что-то ломается и вы открываете тикет в Stripe support, первое, о чём спросят – Request-Id. Без него отладка по обе стороны превращается в угадывание.

Логирование на уровне SDK

SDK может логировать каждый HTTP-запрос и ответ. Он принимает PSR-3 логгер (пакет psr/log; большинство фреймворков его уже подтягивают, для standalone-скриптов нужен composer require psr/log). В dev-окружении прикрепите логгер для трассировки:

\Stripe\Stripe::setLogger(new class extends \Psr\Log\AbstractLogger {
    public function log($level, \Stringable|string $message, array $context = []): void
    {
        error_log("[Stripe {$level}] {$message}");
    }
});

В production выставьте уровень error или отключите логирование полностью. Полный лог запросов/ответов содержит fingerprint-ы карт и email-ы клиентов – данные, которым не место в лог-файле.

Частые паттерны

Проверка существования клиента перед созданием

$existing = $stripe->customers->search([
    'query' => "email:'[email protected]'",
]);

if ($existing->data) {
    $customer = $existing->data[0];
} else {
    $customer = $stripe->customers->create([
        'email' => '[email protected]',
    ]);
}

Search API – eventually consistent. Клиент, созданный секунду назад, может не появиться в результатах поиска. Для real-time проверок храните Stripe customer ID в своей базе и запрашивайте по ID.

Обновление платёжного метода по умолчанию

$stripe->customers->update('cus_abc123', [
    'invoice_settings' => [
        'default_payment_method' => 'pm_newCard456',
    ],
]);

Старое поле default_source работает для legacy-интеграций, но invoice_settings.default_payment_method – актуальный путь. Подписки и инвойсы берут карту оттуда.

Получение charge с разбивкой комиссии

$charge = $stripe->charges->retrieve('ch_abc', [
    'expand' => ['balance_transaction'],
]);

$fee     = $charge->balance_transaction->fee;         // комиссия в центах
$net     = $charge->balance_transaction->net;          // сумма минус комиссии
$details = $charge->balance_transaction->fee_details;  // детализация: stripe fee, tax и т.д.

Комиссия Stripe живёт в balance_transaction, а не на Charge или PaymentIntent напрямую. Логика расчёта комиссий подробнее описана в гайде по тарифам.

Поддерживает ли Stripe PHP?

Да. stripe/stripe-php – SDK от самого Stripe, а не community-обёртка. Обновления выходят в течение дней после изменений API, SDK предоставляет типизированные исключения, автопагинацию и десериализацию ответов. Установка через Composer (composer require stripe/stripe-php), подключение vendor/autoload.php, и все классы пространства \Stripe доступны. Если Composer не вариант (shared hosting, legacy), SDK включает загрузчик init.php, который подменяет autoload. Оба пути описаны в гайде по интеграции платежей.

Что такое Stripe SDK?

SDK (Software Development Kit) – PHP-библиотека, которая берёт на себя HTTP-обвязку общения с REST API Stripe. Вместо ручных curl-запросов, настройки заголовков, парсинга JSON и обработки ошибок, SDK даёт вызов вида $stripe->customers->create([...]) и возвращает типизированный объект. Внутри всё равно HTTPS, но SDK управляет аутентификацией, сериализацией, retry и обработкой ошибок.

Stripe публикует SDK для PHP, Python, Ruby, Node.js, Java, Go и .NET. PHP SDK поддерживается самим Stripe, не сообществом. Причина, по которой API Stripe ценят разработчики – единообразие: каждый ресурс следует одному CRUD-паттерну, каждая ошибка возвращает структурированный JSON с машиночитаемым кодом, а тестовый режим – полная реплика production. SDK наследует это единообразие и добавляет PHP-удобства сверху.

Открыт ли исходный код Stripe API?

Сам API проприетарный, но PHP SDK открыт под MIT-лицензией. Исходный код на github.com/stripe/stripe-php: можно читать каждую строку, заводить issue, отправлять pull request. SDK – это HTTP-клиент; API за ним – закрытый сервис.

Что дальше

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

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