Skip to content
Open
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ jobs:
run: composer install --prefer-dist --no-progress

- name: Run phpstan
run: just lint
run: just lint-test lint

tests:
runs-on: "ubuntu-24.04"
Expand Down
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,47 @@ This release changes the pinned API version to 2026-06-24.preview.
* ⚠️ Remove support for event notifications `V2CoreAccountIncludingConfigurationStorerCapabilityStatusUpdatedEvent` and `V2CoreAccountIncludingConfigurationStorerUpdatedEvent` with related object `V2.Core.Account`
* Add support for error codes `anomalous_money_movement_request`, `failed_tax_calculation`, `financial_account_balance_does_not_support_currency`, `financial_account_capability_not_enabled`, and `financial_account_capability_restricted` on `QuotePreviewInvoice.last_finalization_error`

## 20.3.1 - 2026-07-09
* [#2093](https://github.com/stripe/stripe-php/pull/2093) Add TStripeObject to iterator PHPDoc comments (fixes [#2091](https://github.com/stripe/stripe-php/issues/2091))
- Fixed: PHPStan no longer infers iterated Collection values as `mixed`; loop variables are now correctly typed as the collection's generic type parameter

## 20.3.0 - 2026-06-24
This release changes the pinned API version to 2026-06-24.dahlia.

* [#2088](https://github.com/stripe/stripe-php/pull/2088) Update generated code
* Add support for `release_details` on `Reserve.Hold`
* Add support for new value `tax_fund` on enum `BalanceTransaction.type`
* Change `Billing.CreditGrant.priority` to be required
* Add support for `buyer_id` on `Charge.payment_method_details.bizum`, `ConfirmationToken.payment_method_preview.bizum`, `ConfirmationToken.payment_method_preview.blik`, `PaymentAttemptRecord.payment_method_details.bizum`, `PaymentMethod.bizum`, `PaymentMethod.blik`, and `PaymentRecord.payment_method_details.bizum`
* Add support for `transaction_link_id` on `Charge.payment_method_details.card`
* Add support for new value `sui` on enums `Charge.payment_method_details.crypto.network`, `PaymentAttemptRecord.payment_method_details.crypto.network`, and `PaymentRecord.payment_method_details.crypto.network`
* Add support for new value `usdsui` on enums `Charge.payment_method_details.crypto.token_currency`, `PaymentAttemptRecord.payment_method_details.crypto.token_currency`, and `PaymentRecord.payment_method_details.crypto.token_currency`
* Add support for `fingerprint` on `Charge.payment_method_details.pix`, `ConfirmationToken.payment_method_preview.pix`, `PaymentMethod.pix`, and `SetupAttempt.payment_method_details.pix`
* Add support for `sunbit` on `Checkout.Session.payment_method_options`, `Checkout\Session.create().$params.payment_method_option`, `PaymentIntent.confirm().$params.payment_method_option`, `PaymentIntent.create().$params.payment_method_option`, `PaymentIntent.payment_method_options`, and `PaymentIntent.update().$params.payment_method_option`
* Add support for `billing_cycle_anchor_config` on `Checkout\Session.create().$params.subscription_datum`
* Add support for `wechat_pay` on `Checkout.Session.payment_method_options`
* Add support for `mastercard_compliance` on `Dispute.evidence.enhanced_evidence`, `Dispute.evidence_details.enhanced_eligibility`, and `Dispute.update().$params.evidence.enhanced_evidence`
* Add support for new value `mastercard_compliance` on enum `Dispute.enhanced_eligibility_types`
* Add support for `status_details` on `FinancialConnections.Account`
* Add support for new value `validated` on enum `Identity.VerificationSession.redaction.status`
* Add support for new value `satispay` on enums `Invoice.payment_settings.payment_method_types` and `Subscription.payment_settings.payment_method_types`
* ⚠️ Remove support for `stored_credential_usage` on `PaymentAttemptRecord.payment_method_details.card` and `PaymentRecord.payment_method_details.card`
* ⚠️ Change `PaymentAttemptRecord.payment_method_details.card.description` and `PaymentRecord.payment_method_details.card.description` to be optional
* ⚠️ Change `PaymentAttemptRecord.payment_method_details.card.iin` and `PaymentRecord.payment_method_details.card.iin` to be optional
* ⚠️ Change `PaymentAttemptRecord.payment_method_details.card.issuer` and `PaymentRecord.payment_method_details.card.issuer` to be optional
* Add support for `setup_future_usage` on `PaymentIntent.confirm().$params.payment_method_option.satispay`, `PaymentIntent.create().$params.payment_method_option.satispay`, `PaymentIntent.payment_method_options.satispay`, and `PaymentIntent.update().$params.payment_method_option.satispay`
* Change `PaymentRecord.report_refund().$params.refunded` to be optional
* Add support for `satispay` on `SetupAttempt.payment_method_details`
* Add support for `custom_fields`, `description`, and `footer` on `Subscription.create().$params.invoice_setting`, `Subscription.invoice_settings`, and `Subscription.update().$params.invoice_setting`
* Add support for `payment_method_options` and `payment_method` on `Topup.create().$params`
* Add support for `mode` on `V2.Commerce.ProductCatalogImport`
* Add support for new value `promotion` on enum `V2.Commerce.ProductCatalogImport.feed_type`
* Add support for `sunbit_payments` on `V2.Core.Account.configuration.merchant.capabilities`, `V2\Core\Account.create().$params.configuration.merchant.capability`, and `V2\Core\Account.update().$params.configuration.merchant.capability`
* Add support for `crypto_money_manager` and `money_manager` on `V2\Core\Account.update().$params.identity.attestation.terms_of_service`
* ⚠️ Remove support for `crypto_storer` and `storer` on `V2\Core\Account.update().$params.identity.attestation.terms_of_service`
* Add support for new value `sunbit_payments` on enum `EventsV2CoreAccountIncludingConfigurationMerchantCapabilityStatusUpdatedEvent.updated_capability`
* Add support for error codes `anomalous_money_movement_request`, `failed_tax_calculation`, `financial_account_balance_does_not_support_currency`, `financial_account_capability_not_enabled`, and `financial_account_capability_restricted` on `Invoice.last_finalization_error`, `PaymentIntent.last_payment_error`, `SetupAttempt.setup_error`, `SetupIntent.last_setup_error`, `StripeError`, and `Terminal.Reader.action.api_error`

## 20.3.0-beta.1 - 2026-05-27
This release changes the pinned API version to 2026-05-27.private.

Expand Down
2 changes: 1 addition & 1 deletion CODEGEN_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6012b623b1c09ad54d466947da04511a042ee45a
3723bdf073100308cd413efdb4aefbc912f9bf2f
2 changes: 1 addition & 1 deletion OPENAPI_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2324
v2343
38 changes: 38 additions & 0 deletions examples/IteratorExample.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

/**
* This is a simple example to get and list customers.
*
* It is used by lint-test to ensure that the iterated value type is not resolved to mixed
*/
require 'vendor/autoload.php'; // Make sure to include Composer's autoload file
// Use this require statement to import the SDK from this repo folder
// require '../init.php';

class IteratorExample
{
private string $apiKey;

public function __construct(string $apiKey)
{
$this->apiKey = $apiKey;
}

public function iterateOverCustomers(): void
{
$client = new Stripe\StripeClient($this->apiKey);
$customers = $client->customers->all();
foreach ($customers as $customer) {
echo $customer->email;
}
foreach ($customers->autoPagingIterator() as $customer) {
echo $customer->id;
}
}
}

// Usage
$apiKey = '{{API_KEY}}';

$example = new IteratorExample($apiKey);
$example->iterateOverCustomers();
4 changes: 4 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ format-check: (format "--dry-run")
lint *args:
php -d memory_limit=512M vendor/bin/phpstan analyse lib tests {{args}}

# statically analyze code (strict)
lint-test:
phpstan analyse examples/IteratorExample.php --level=9

# for backwards compatibility; ideally removed later
[private]
alias phpstan := lint
Expand Down
4 changes: 2 additions & 2 deletions lib/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public function count()
}

/**
* @return \ArrayIterator an iterator that can be used to iterate
* @return \ArrayIterator<int, TStripeObject> an iterator that can be used to iterate
* across objects in the current page
*/
#[\ReturnTypeWillChange]
Expand All @@ -157,7 +157,7 @@ public function getIterator()
}

/**
* @return \ArrayIterator an iterator that can be used to iterate
* @return \ArrayIterator<int, TStripeObject> an iterator that can be used to iterate
* backwards across objects in the current page
*/
public function getReverseIterator()
Expand Down
19 changes: 4 additions & 15 deletions lib/HttpClient/CurlClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ public function executeStreamingRequestWithRetries($opts, $absUrl, $readBodyChun

if ($shouldRetry) {
++$numRetries;
$sleepSeconds = $this->sleepTime($numRetries, $lastRHeaders);
$sleepSeconds = $this->sleepTime($numRetries);
\usleep((int) ($sleepSeconds * 1000000));
} else {
break;
Expand Down Expand Up @@ -592,7 +592,7 @@ public function executeRequestWithRetries($opts, $absUrl, $maxNetworkRetries = n

if ($shouldRetry) {
++$numRetries;
$sleepSeconds = $this->sleepTime($numRetries, $rheaders);
$sleepSeconds = $this->sleepTime($numRetries);
\usleep((int) ($sleepSeconds * 1000000));
} else {
break;
Expand Down Expand Up @@ -719,11 +719,10 @@ private function shouldRetry($errno, $rcode, $rheaders, $numRetries, $maxNetwork
* Provides the number of seconds to wait before retrying a request.
*
* @param int $numRetries
* @param array|Util\CaseInsensitiveArray $rheaders
*
* @return int
*/
private function sleepTime($numRetries, $rheaders)
private function sleepTime($numRetries)
{
// Apply exponential backoff with $initialNetworkRetryDelay on the
// number of $numRetries so far as inputs. Do not allow the number to exceed
Expand All @@ -735,18 +734,8 @@ private function sleepTime($numRetries, $rheaders)

// Apply some jitter by randomizing the value in the range of
// ($sleepSeconds / 2) to ($sleepSeconds).
$sleepSeconds *= 0.5 * (1 + $this->randomGenerator->randFloat());

// But never sleep less than the base sleep seconds.
$sleepSeconds = \max(Stripe::getInitialNetworkRetryDelay(), $sleepSeconds);

// And never sleep less than the time the API asks us to wait, assuming it's a reasonable ask.
$retryAfter = isset($rheaders['retry-after']) ? (float) ($rheaders['retry-after']) : 0.0;
if (\floor($retryAfter) === $retryAfter && $retryAfter <= Stripe::getMaxRetryAfter()) {
$sleepSeconds = \max($sleepSeconds, $retryAfter);
}

return $sleepSeconds;
return \max(Stripe::getInitialNetworkRetryDelay(), $sleepSeconds * 0.5 * (1 + $this->randomGenerator->randFloat()));
}

/**
Expand Down
11 changes: 0 additions & 11 deletions lib/Stripe.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,6 @@ class Stripe
/** @var float Maximum delay between retries, in seconds */
private static $maxNetworkRetryDelay = 2.0;

/** @var float Maximum delay between retries, in seconds, that will be respected from the Stripe API */
private static $maxRetryAfter = 60.0;

/** @var float Initial delay between retries, in seconds */
private static $initialNetworkRetryDelay = 0.5;

Expand Down Expand Up @@ -274,14 +271,6 @@ public static function getMaxNetworkRetryDelay()
return self::$maxNetworkRetryDelay;
}

/**
* @return float Maximum delay between retries, in seconds, that will be respected from the Stripe API
*/
public static function getMaxRetryAfter()
{
return self::$maxRetryAfter;
}

/**
* @return float Initial delay between retries, in seconds
*/
Expand Down
39 changes: 12 additions & 27 deletions tests/Stripe/HttpClient/CurlClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -312,19 +312,19 @@ public function testSleepTimeShouldGrowExponentially()

self::assertSame(
\Stripe\Stripe::getInitialNetworkRetryDelay() * 1,
$this->sleepTimeMethod->invoke($curlClient, 1, [])
$this->sleepTimeMethod->invoke($curlClient, 1)
);
self::assertSame(
\Stripe\Stripe::getInitialNetworkRetryDelay() * 2,
$this->sleepTimeMethod->invoke($curlClient, 2, [])
$this->sleepTimeMethod->invoke($curlClient, 2)
);
self::assertSame(
\Stripe\Stripe::getInitialNetworkRetryDelay() * 4,
$this->sleepTimeMethod->invoke($curlClient, 3, [])
$this->sleepTimeMethod->invoke($curlClient, 3)
);
self::assertSame(
\Stripe\Stripe::getInitialNetworkRetryDelay() * 8,
$this->sleepTimeMethod->invoke($curlClient, 4, [])
$this->sleepTimeMethod->invoke($curlClient, 4)
);
}

Expand All @@ -335,25 +335,10 @@ public function testSleepTimeShouldEnforceMaxNetworkRetryDelay()

$curlClient = new CurlClient(null, $this->createFakeRandomGenerator());

self::assertSame(1.0, $this->sleepTimeMethod->invoke($curlClient, 1, []));
self::assertSame(2.0, $this->sleepTimeMethod->invoke($curlClient, 2, []));
self::assertSame(2.0, $this->sleepTimeMethod->invoke($curlClient, 3, []));
self::assertSame(2.0, $this->sleepTimeMethod->invoke($curlClient, 4, []));
}

public function testSleepTimeShouldRespectRetryAfter()
{
$this->setInitialNetworkRetryDelay(1.0);
$this->setMaxNetworkRetryDelay(2.0);

$curlClient = new CurlClient(null, $this->createFakeRandomGenerator());

// Uses max of default and header.
self::assertSame(10.0, $this->sleepTimeMethod->invoke($curlClient, 1, ['retry-after' => '10']));
self::assertSame(2.0, $this->sleepTimeMethod->invoke($curlClient, 2, ['retry-after' => '1']));

// Ignores excessively large values.
self::assertSame(2.0, $this->sleepTimeMethod->invoke($curlClient, 2, ['retry-after' => '100']));
self::assertSame(1.0, $this->sleepTimeMethod->invoke($curlClient, 1));
self::assertSame(2.0, $this->sleepTimeMethod->invoke($curlClient, 2));
self::assertSame(2.0, $this->sleepTimeMethod->invoke($curlClient, 3));
self::assertSame(2.0, $this->sleepTimeMethod->invoke($curlClient, 4));
}

public function testSleepTimeShouldAddSomeRandomness()
Expand All @@ -368,12 +353,12 @@ public function testSleepTimeShouldAddSomeRandomness()

// the initial value cannot be smaller than the base,
// so the randomness is ignored
self::assertSame(\Stripe\Stripe::getInitialNetworkRetryDelay(), $this->sleepTimeMethod->invoke($curlClient, 1, []));
self::assertSame(\Stripe\Stripe::getInitialNetworkRetryDelay(), $this->sleepTimeMethod->invoke($curlClient, 1));

// after the first one, the randomness is applied
self::assertSame($baseValue * 2, $this->sleepTimeMethod->invoke($curlClient, 2, []));
self::assertSame($baseValue * 4, $this->sleepTimeMethod->invoke($curlClient, 3, []));
self::assertSame($baseValue * 8, $this->sleepTimeMethod->invoke($curlClient, 4, []));
self::assertSame($baseValue * 2, $this->sleepTimeMethod->invoke($curlClient, 2));
self::assertSame($baseValue * 4, $this->sleepTimeMethod->invoke($curlClient, 3));
self::assertSame($baseValue * 8, $this->sleepTimeMethod->invoke($curlClient, 4));
}

/**
Expand Down
Loading