diff --git a/CHANGELOG.md b/CHANGELOG.md
index f97e9df9cc7..765a923ac62 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,46 @@ This release changes the pinned API version to 2026-07-29.preview.
* Add support for `payout` on `v2.moneymanagement.ReceivedCredit.balance_transfer`
* ⚠️ Remove support for `payoutV1` on `v2.moneymanagement.ReceivedCredit.balance_transfer`
+## 33.2.0 - 2026-07-29
+This release changes the pinned API version to 2026-07-29.dahlia.
+
+* [#2257](https://github.com/stripe/stripe-java/pull/2257) Update generated code
+ * Add support for new resource `financialconnections.Authorization`
+ * Add support for `unreject` method on resource `Account`
+ * Add support for `list` method on resource `PaymentRecord`
+ * Add support for `smartDisputesManagement` on `AccountSession.components.disputes_list.features`, `AccountSession.components.payment_details.features`, `AccountSession.components.payment_disputes.features`, `AccountSession.components.payments.features`, `AccountSessionCreateParams.components.disputes_list.features`, `AccountSessionCreateParams.components.payment_details.features`, `AccountSessionCreateParams.components.payment_disputes.features`, and `AccountSessionCreateParams.components.payments.features`
+ * Add support for `administrativeAddress` and `principalPlaceOfBusiness` on `Account.company`, `AccountCreateParams.company`, `AccountUpdateParams.company`, and `TokenCreateParams.account.company`
+ * Add support for `sepaDebitPayments` on `AccountUpdateParams.settings`
+ * Remove support for `proofOfRegistration` on `AccountCreateParams.documents`. This field was limited-use and is being deprecated.
+ * Add support for `payoutsAction` on `AccountRejectParams`
+ * Remove support for `dynamicTaxRates` on `checkout.SessionCreateParams.line_items[]`. This field was limited-use and is being deprecated.
+ * Add support for `setupFutureUsage` on `PaymentIntent.payment_method_options.payco`, `PaymentIntent.payment_method_options.samsung_pay`, `PaymentIntentConfirmParams.payment_method_options.payco`, `PaymentIntentConfirmParams.payment_method_options.samsung_pay`, `PaymentIntentCreateParams.payment_method_options.payco`, `PaymentIntentCreateParams.payment_method_options.samsung_pay`, `PaymentIntentUpdateParams.payment_method_options.payco`, `PaymentIntentUpdateParams.payment_method_options.samsung_pay`, `PaymentLinkUpdateParams.payment_intent_data`, `checkout.Session.payment_method_options.payco`, `checkout.Session.payment_method_options.samsung_pay`, `checkout.SessionCreateParams.payment_method_options.payco`, and `checkout.SessionCreateParams.payment_method_options.samsung_pay`
+ * Add support for new values `bnp_paribas`, `citibank`, and `mbsb_bank` on enums `ConfirmationTokenCreateParams.payment_method_data.fpx.bank`, `PaymentIntentConfirmParams.payment_method_data.fpx.bank`, `PaymentIntentCreateParams.payment_method_data.fpx.bank`, `PaymentIntentUpdateParams.payment_method_data.fpx.bank`, `PaymentMethodCreateParams.fpx.bank`, `SetupIntentConfirmParams.payment_method_data.fpx.bank`, `SetupIntentCreateParams.payment_method_data.fpx.bank`, and `SetupIntentUpdateParams.payment_method_data.fpx.bank`
+ * Add support for new value `ic_nif` on enums `CustomerCreateParams.tax_id_data[].type`, `InvoiceCreatePreviewParams.customer_details.tax_ids[].type`, `TaxIdCreateParams.type`, and `tax.CalculationCreateParams.customer_details.tax_ids[].type`
+ * Add support for `network` on `Dispute.payment_method_details.card`
+ * Add support for `limits` and `manualEntry` on `financialconnections.SessionCreateParams` and `financialconnections.Session`
+ * Add support for `requirePaymentMethodSupport` on `financialconnections.Session.filters` and `financialconnections.SessionCreateParams.filters`
+ * Add support for `bankAccountToken` on `financialconnections.Session`
+ * Add support for new values `alipay` and `mb_way` on enums `InvoiceCreateParams.payment_settings.paymentMethodTypes`, `InvoiceUpdateParams.payment_settings.paymentMethodTypes`, `SubscriptionCreateParams.payment_settings.paymentMethodTypes`, and `SubscriptionUpdateParams.payment_settings.paymentMethodTypes`
+ * Add support for new values `mass_transit_parking_tax` and `parking_tax` on enums `InvoiceAddLinesParams.lines[].tax_amounts[].tax_rate_data.taxType`, `InvoiceLineItemUpdateParams.tax_amounts[].tax_rate_data.taxType`, `InvoiceUpdateLinesParams.lines[].tax_amounts[].tax_rate_data.taxType`, `TaxRateCreateParams.taxType`, and `TaxRateUpdateParams.taxType`
+ * Add support for `metadata` on `InvoiceCreatePreviewParams.subscription_details`
+ * Add support for `businessName` on `issuing.Card.shipping`, `issuing.CardCreateParams.shipping`, and `issuing.CardUpdateParams.shipping`
+ * Add support for `allowedPaymentMethodTypes` on `PaymentIntentConfirmParams`, `PaymentIntentCreateParams`, `PaymentIntentUpdateParams`, `PaymentIntent`, `SetupIntentConfirmParams`, `SetupIntentCreateParams`, `SetupIntentUpdateParams`, and `SetupIntent`
+ * Add support for `referrer` on `PaymentIntentConfirmParams.radar_options` and `PaymentIntentCreateParams.radar_options`
+ * Add support for `consentCollection` and `shippingOptions` on `PaymentLinkUpdateParams`
+ * Add support for `customFields`, `description`, and `footer` on `Quote.invoice_settings`, `QuoteCreateParams.invoice_settings`, `QuoteUpdateParams.invoice_settings`, `SubscriptionSchedule.default_settings.invoice_settings`, `SubscriptionSchedule.phases[].invoice_settings`, `SubscriptionScheduleCreateParams.default_settings.invoice_settings`, `SubscriptionScheduleCreateParams.phases[].invoice_settings`, `SubscriptionScheduleUpdateParams.default_settings.invoice_settings`, and `SubscriptionScheduleUpdateParams.phases[].invoice_settings`
+ * Add support for `customerAccount` and `customer` on `Refund`
+ * Add support for `paymentMethod` on `Refund` and `Topup`
+ * Add support for `trial` on `SubscriptionSchedule.phases[]`
+ * Add support for `massTransitParkingTax` and `parkingTax` on `tax.Registration.country_options.us` and `tax.RegistrationCreateParams.country_options.us`
+ * Add support for new values `mass_transit_parking_tax` and `parking_tax` on enum `tax.RegistrationCreateParams.country_options.us.type`
+ * Add support for `initiatedBy` and `paymentMethodOptions` on `Topup`
+ * Add support for new values `financial_connections.account.expected_deactivation_date_updated`, `financial_connections.account.supported_payment_method_types_updated`, `financial_connections.account.upcoming_deactivation`, `financial_connections.authorization.expected_deactivation_date_updated`, and `financial_connections.authorization.upcoming_deactivation` on enums `WebhookEndpointCreateParams.enabledEvents` and `WebhookEndpointUpdateParams.enabledEvents`
+ * Add support for new value `2026-07-29.dahlia` on enum `WebhookEndpointCreateParams.apiVersion`
+ * Add support for `additionalAddresses` on `v2.core.Account.identity.business_details`, `v2.core.AccountCreateParams.identity.business_details`, `v2.core.AccountTokenCreateParams.identity.business_details`, and `v2.core.AccountUpdateParams.identity.business_details`
+ * Add support for snapshot events `financial_connections.account.expected_deactivation_date_updated`, `financial_connections.account.supported_payment_method_types_updated`, and `financial_connections.account.upcoming_deactivation` with resource `financialconnections.Account`
+ * Add support for snapshot events `financial_connections.authorization.expected_deactivation_date_updated` and `financial_connections.authorization.upcoming_deactivation` with resource `financialconnections.Authorization`
+
## 33.2.0-beta.1 - 2026-06-24
This release changes the pinned API version to 2026-06-24.preview.
diff --git a/CODEGEN_VERSION b/CODEGEN_VERSION
index ea710b3c849..46dccda447e 100644
--- a/CODEGEN_VERSION
+++ b/CODEGEN_VERSION
@@ -1 +1 @@
-44014016ec56efb57a8cf6d0ee0771c2e6b4eebe
\ No newline at end of file
+7b0e14a8a4b606fa2cc641579a18e077e25aaac9
\ No newline at end of file
diff --git a/OPENAPI_VERSION b/OPENAPI_VERSION
index 5ea23263dda..d7096ed4ff2 100644
--- a/OPENAPI_VERSION
+++ b/OPENAPI_VERSION
@@ -1 +1 @@
-v2349
\ No newline at end of file
+v2391
\ No newline at end of file
diff --git a/src/main/java/com/stripe/ApiVersion.java b/src/main/java/com/stripe/ApiVersion.java
index 927c6ed2138..42f0bb9f638 100644
--- a/src/main/java/com/stripe/ApiVersion.java
+++ b/src/main/java/com/stripe/ApiVersion.java
@@ -3,4 +3,5 @@
final class ApiVersion {
public static final String CURRENT = "2026-07-29.preview";
+ public static final String CURRENT_MAJOR = "";
}
diff --git a/src/main/java/com/stripe/Stripe.java b/src/main/java/com/stripe/Stripe.java
index 3dbc17c2b5b..05bd86bb0c4 100644
--- a/src/main/java/com/stripe/Stripe.java
+++ b/src/main/java/com/stripe/Stripe.java
@@ -11,6 +11,12 @@ public abstract class Stripe {
public static final int DEFAULT_READ_TIMEOUT = 80 * 1000;
public static final String API_VERSION = ApiVersion.CURRENT;
+ /**
+ * The major API version that this SDK uses. Objects retrieved using the same major version are
+ * compatible. Is an empty string in preview versions of the SDK.
+ */
+ public static final String MAJOR_API_VERSION = ApiVersion.CURRENT_MAJOR;
+
public static final String CONNECT_API_BASE = "https://connect.stripe.com";
public static final String LIVE_API_BASE = "https://api.stripe.com";
public static final String UPLOAD_API_BASE = "https://files.stripe.com";
diff --git a/src/main/java/com/stripe/StripeClient.java b/src/main/java/com/stripe/StripeClient.java
index 40120655009..2aafbb09770 100644
--- a/src/main/java/com/stripe/StripeClient.java
+++ b/src/main/java/com/stripe/StripeClient.java
@@ -2,6 +2,7 @@
import com.stripe.exception.SignatureVerificationException;
import com.stripe.exception.StripeException;
+import com.stripe.model.Event;
import com.stripe.model.StripeObject;
import com.stripe.model.v2.core.EventNotification;
import com.stripe.net.*;
@@ -115,9 +116,10 @@ public EventNotification parseEventNotification(String payload, String sigHeader
}
/**
- * Returns an StripeEvent instance using the provided JSON payload. Throws a JsonSyntaxException
- * if the payload is not valid JSON, and a SignatureVerificationException if the signature
- * verification fails for any reason.
+ * Constructs a thin event
+ * notification from an incoming webhook after verifying its authenticity. To work with a
+ * webhook that has already been verified (i.e. one from a cloud provider, an asynchronous queue,
+ * or during testing), see {@code parseEventNotificationWithoutVerification}.
*
* @param payload the payload sent by Stripe.
* @param sigHeader the contents of the signature header sent by Stripe.
@@ -174,6 +176,45 @@ public com.stripe.model.Event constructEvent(
return event;
}
+ /**
+ * Constructs a snapshot
+ * event from an incoming webhook without first verifying its authenticity. Should be used
+ * after calling {@code Webhook.Signature.verifyHeader(...)} or with input from a trusted source
+ * (such as AWS EventBridge,
+ * or Azure Event Grid
+ * payload). Or, to verify & construct in a single call, use {@code constructEvent(...)}
+ * instead.
+ *
+ * @param payload the JSON payload: a raw Stripe Event or an AWS EventBridge/Azure Event Grid
+ * envelope.
+ * @return the Event instance.
+ * @throws IllegalArgumentException if the payload is a thin event notification, or if the format
+ * is not recognized.
+ */
+ public com.stripe.model.Event constructEventWithoutVerification(String payload) {
+ Event event = Webhook.constructEventWithoutVerification(payload);
+ event.setResponseGetter(this.getResponseGetter());
+ return event;
+ }
+
+ /**
+ * Constructs a thin event
+ * notification from an incoming webhook without first verifying its authenticity. Should be
+ * used after calling {@code Webhook.Signature.verifyHeader(...)} or with input from a trusted
+ * source (such as AWS
+ * EventBridge, or Azure Event
+ * Grid payload). Or, to verify & parse in a single call, use {@code
+ * parseEventNotification(...)} instead.
+ *
+ * @param payload the JSON payload: a raw Stripe Event, or an AWS EventBridge/Azure Event Grid
+ * envelope.
+ * @return the EventNotification instance.
+ * @throws IllegalArgumentException if the payload format is not recognized.
+ */
+ public EventNotification parseEventNotificationWithoutVerification(String payload) {
+ return EventNotification.fromJson(Webhook.maybeExtractFromCloudProviderEnvelope(payload), this);
+ }
+
// The beginning of the section generated from our OpenAPI spec
public com.stripe.service.V1Services v1() {
return new com.stripe.service.V1Services(this.getResponseGetter());
diff --git a/src/main/java/com/stripe/exception/AlreadyCanceledException.java b/src/main/java/com/stripe/exception/AlreadyCanceledException.java
index 8afb343d196..35fd91cd5bc 100644
--- a/src/main/java/com/stripe/exception/AlreadyCanceledException.java
+++ b/src/main/java/com/stripe/exception/AlreadyCanceledException.java
@@ -18,9 +18,8 @@ private AlreadyCanceledException(
static AlreadyCanceledException parse(
JsonObject body, int statusCode, String requestId, StripeResponseGetter responseGetter) {
AlreadyCanceledException.AlreadyCanceledError error =
- (AlreadyCanceledException.AlreadyCanceledError)
- StripeObject.deserializeStripeObject(
- body, AlreadyCanceledException.AlreadyCanceledError.class, responseGetter);
+ StripeObject.deserializeStripeObject(
+ body, AlreadyCanceledException.AlreadyCanceledError.class, responseGetter);
AlreadyCanceledException exception =
new AlreadyCanceledException(
error.getMessage(), requestId, error.getCode(), statusCode, null);
diff --git a/src/main/java/com/stripe/exception/AlreadyExistsException.java b/src/main/java/com/stripe/exception/AlreadyExistsException.java
index 317c4407b9b..84923868085 100644
--- a/src/main/java/com/stripe/exception/AlreadyExistsException.java
+++ b/src/main/java/com/stripe/exception/AlreadyExistsException.java
@@ -18,9 +18,8 @@ private AlreadyExistsException(
static AlreadyExistsException parse(
JsonObject body, int statusCode, String requestId, StripeResponseGetter responseGetter) {
AlreadyExistsException.AlreadyExistsError error =
- (AlreadyExistsException.AlreadyExistsError)
- StripeObject.deserializeStripeObject(
- body, AlreadyExistsException.AlreadyExistsError.class, responseGetter);
+ StripeObject.deserializeStripeObject(
+ body, AlreadyExistsException.AlreadyExistsError.class, responseGetter);
AlreadyExistsException exception =
new AlreadyExistsException(
error.getMessage(), requestId, error.getCode(), statusCode, null);
diff --git a/src/main/java/com/stripe/exception/BlockedByStripeException.java b/src/main/java/com/stripe/exception/BlockedByStripeException.java
index 02181d693c8..b5f3d1d1edf 100644
--- a/src/main/java/com/stripe/exception/BlockedByStripeException.java
+++ b/src/main/java/com/stripe/exception/BlockedByStripeException.java
@@ -18,9 +18,8 @@ private BlockedByStripeException(
static BlockedByStripeException parse(
JsonObject body, int statusCode, String requestId, StripeResponseGetter responseGetter) {
BlockedByStripeException.BlockedByStripeError error =
- (BlockedByStripeException.BlockedByStripeError)
- StripeObject.deserializeStripeObject(
- body, BlockedByStripeException.BlockedByStripeError.class, responseGetter);
+ StripeObject.deserializeStripeObject(
+ body, BlockedByStripeException.BlockedByStripeError.class, responseGetter);
BlockedByStripeException exception =
new BlockedByStripeException(
error.getMessage(), requestId, error.getCode(), statusCode, null);
diff --git a/src/main/java/com/stripe/exception/CannotProceedException.java b/src/main/java/com/stripe/exception/CannotProceedException.java
index 089992b1865..bbc55f76fad 100644
--- a/src/main/java/com/stripe/exception/CannotProceedException.java
+++ b/src/main/java/com/stripe/exception/CannotProceedException.java
@@ -27,9 +27,8 @@ private CannotProceedException(
static CannotProceedException parse(
JsonObject body, int statusCode, String requestId, StripeResponseGetter responseGetter) {
CannotProceedException.CannotProceedError error =
- (CannotProceedException.CannotProceedError)
- StripeObject.deserializeStripeObject(
- body, CannotProceedException.CannotProceedError.class, responseGetter);
+ StripeObject.deserializeStripeObject(
+ body, CannotProceedException.CannotProceedError.class, responseGetter);
CannotProceedException exception =
new CannotProceedException(
error.getMessage(), requestId, error.getCode(), statusCode, null, error.getReason());
diff --git a/src/main/java/com/stripe/exception/ControlledByAlternateResourceException.java b/src/main/java/com/stripe/exception/ControlledByAlternateResourceException.java
index a21b9f0eb35..07e2a29b81a 100644
--- a/src/main/java/com/stripe/exception/ControlledByAlternateResourceException.java
+++ b/src/main/java/com/stripe/exception/ControlledByAlternateResourceException.java
@@ -21,11 +21,10 @@ private ControlledByAlternateResourceException(
static ControlledByAlternateResourceException parse(
JsonObject body, int statusCode, String requestId, StripeResponseGetter responseGetter) {
ControlledByAlternateResourceException.ControlledByAlternateResourceError error =
- (ControlledByAlternateResourceException.ControlledByAlternateResourceError)
- StripeObject.deserializeStripeObject(
- body,
- ControlledByAlternateResourceException.ControlledByAlternateResourceError.class,
- responseGetter);
+ StripeObject.deserializeStripeObject(
+ body,
+ ControlledByAlternateResourceException.ControlledByAlternateResourceError.class,
+ responseGetter);
ControlledByAlternateResourceException exception =
new ControlledByAlternateResourceException(
error.getMessage(), requestId, error.getCode(), statusCode, null);
diff --git a/src/main/java/com/stripe/exception/ControlledByDashboardException.java b/src/main/java/com/stripe/exception/ControlledByDashboardException.java
index cac7b66e44b..a65bf8542ef 100644
--- a/src/main/java/com/stripe/exception/ControlledByDashboardException.java
+++ b/src/main/java/com/stripe/exception/ControlledByDashboardException.java
@@ -21,11 +21,8 @@ private ControlledByDashboardException(
static ControlledByDashboardException parse(
JsonObject body, int statusCode, String requestId, StripeResponseGetter responseGetter) {
ControlledByDashboardException.ControlledByDashboardError error =
- (ControlledByDashboardException.ControlledByDashboardError)
- StripeObject.deserializeStripeObject(
- body,
- ControlledByDashboardException.ControlledByDashboardError.class,
- responseGetter);
+ StripeObject.deserializeStripeObject(
+ body, ControlledByDashboardException.ControlledByDashboardError.class, responseGetter);
ControlledByDashboardException exception =
new ControlledByDashboardException(
error.getMessage(), requestId, error.getCode(), statusCode, null);
diff --git a/src/main/java/com/stripe/exception/FeatureNotEnabledException.java b/src/main/java/com/stripe/exception/FeatureNotEnabledException.java
index a1e24a0e4d1..1da536ba497 100644
--- a/src/main/java/com/stripe/exception/FeatureNotEnabledException.java
+++ b/src/main/java/com/stripe/exception/FeatureNotEnabledException.java
@@ -18,9 +18,8 @@ private FeatureNotEnabledException(
static FeatureNotEnabledException parse(
JsonObject body, int statusCode, String requestId, StripeResponseGetter responseGetter) {
FeatureNotEnabledException.FeatureNotEnabledError error =
- (FeatureNotEnabledException.FeatureNotEnabledError)
- StripeObject.deserializeStripeObject(
- body, FeatureNotEnabledException.FeatureNotEnabledError.class, responseGetter);
+ StripeObject.deserializeStripeObject(
+ body, FeatureNotEnabledException.FeatureNotEnabledError.class, responseGetter);
FeatureNotEnabledException exception =
new FeatureNotEnabledException(
error.getMessage(), requestId, error.getCode(), statusCode, null);
diff --git a/src/main/java/com/stripe/exception/FinancialAccountNotOpenException.java b/src/main/java/com/stripe/exception/FinancialAccountNotOpenException.java
index 66a745010d4..a4feb652832 100644
--- a/src/main/java/com/stripe/exception/FinancialAccountNotOpenException.java
+++ b/src/main/java/com/stripe/exception/FinancialAccountNotOpenException.java
@@ -17,11 +17,10 @@ private FinancialAccountNotOpenException(
static FinancialAccountNotOpenException parse(
JsonObject body, int statusCode, String requestId, StripeResponseGetter responseGetter) {
FinancialAccountNotOpenException.FinancialAccountNotOpenError error =
- (FinancialAccountNotOpenException.FinancialAccountNotOpenError)
- StripeObject.deserializeStripeObject(
- body,
- FinancialAccountNotOpenException.FinancialAccountNotOpenError.class,
- responseGetter);
+ StripeObject.deserializeStripeObject(
+ body,
+ FinancialAccountNotOpenException.FinancialAccountNotOpenError.class,
+ responseGetter);
FinancialAccountNotOpenException exception =
new FinancialAccountNotOpenException(
error.getMessage(), requestId, error.getCode(), statusCode, null);
diff --git a/src/main/java/com/stripe/exception/InsufficientFundsException.java b/src/main/java/com/stripe/exception/InsufficientFundsException.java
index e28c0065d0d..a640ffd962f 100644
--- a/src/main/java/com/stripe/exception/InsufficientFundsException.java
+++ b/src/main/java/com/stripe/exception/InsufficientFundsException.java
@@ -21,9 +21,8 @@ private InsufficientFundsException(
static InsufficientFundsException parse(
JsonObject body, int statusCode, String requestId, StripeResponseGetter responseGetter) {
InsufficientFundsException.InsufficientFundsError error =
- (InsufficientFundsException.InsufficientFundsError)
- StripeObject.deserializeStripeObject(
- body, InsufficientFundsException.InsufficientFundsError.class, responseGetter);
+ StripeObject.deserializeStripeObject(
+ body, InsufficientFundsException.InsufficientFundsError.class, responseGetter);
InsufficientFundsException exception =
new InsufficientFundsException(
error.getMessage(), requestId, error.getCode(), statusCode, null);
diff --git a/src/main/java/com/stripe/exception/InvalidPaymentMethodException.java b/src/main/java/com/stripe/exception/InvalidPaymentMethodException.java
index 320cee6e260..49cfb87a998 100644
--- a/src/main/java/com/stripe/exception/InvalidPaymentMethodException.java
+++ b/src/main/java/com/stripe/exception/InvalidPaymentMethodException.java
@@ -30,11 +30,8 @@ private InvalidPaymentMethodException(
static InvalidPaymentMethodException parse(
JsonObject body, int statusCode, String requestId, StripeResponseGetter responseGetter) {
InvalidPaymentMethodException.InvalidPaymentMethodError error =
- (InvalidPaymentMethodException.InvalidPaymentMethodError)
- StripeObject.deserializeStripeObject(
- body,
- InvalidPaymentMethodException.InvalidPaymentMethodError.class,
- responseGetter);
+ StripeObject.deserializeStripeObject(
+ body, InvalidPaymentMethodException.InvalidPaymentMethodError.class, responseGetter);
InvalidPaymentMethodException exception =
new InvalidPaymentMethodException(
error.getMessage(),
diff --git a/src/main/java/com/stripe/exception/InvalidPayoutMethodException.java b/src/main/java/com/stripe/exception/InvalidPayoutMethodException.java
index 1b92d726743..d25634cc672 100644
--- a/src/main/java/com/stripe/exception/InvalidPayoutMethodException.java
+++ b/src/main/java/com/stripe/exception/InvalidPayoutMethodException.java
@@ -18,9 +18,8 @@ private InvalidPayoutMethodException(
static InvalidPayoutMethodException parse(
JsonObject body, int statusCode, String requestId, StripeResponseGetter responseGetter) {
InvalidPayoutMethodException.InvalidPayoutMethodError error =
- (InvalidPayoutMethodException.InvalidPayoutMethodError)
- StripeObject.deserializeStripeObject(
- body, InvalidPayoutMethodException.InvalidPayoutMethodError.class, responseGetter);
+ StripeObject.deserializeStripeObject(
+ body, InvalidPayoutMethodException.InvalidPayoutMethodError.class, responseGetter);
InvalidPayoutMethodException exception =
new InvalidPayoutMethodException(
error.getMessage(), requestId, error.getCode(), statusCode, null);
diff --git a/src/main/java/com/stripe/exception/NonZeroBalanceException.java b/src/main/java/com/stripe/exception/NonZeroBalanceException.java
index e9ffb6bc220..0fe7bcff5ad 100644
--- a/src/main/java/com/stripe/exception/NonZeroBalanceException.java
+++ b/src/main/java/com/stripe/exception/NonZeroBalanceException.java
@@ -18,9 +18,8 @@ private NonZeroBalanceException(
static NonZeroBalanceException parse(
JsonObject body, int statusCode, String requestId, StripeResponseGetter responseGetter) {
NonZeroBalanceException.NonZeroBalanceError error =
- (NonZeroBalanceException.NonZeroBalanceError)
- StripeObject.deserializeStripeObject(
- body, NonZeroBalanceException.NonZeroBalanceError.class, responseGetter);
+ StripeObject.deserializeStripeObject(
+ body, NonZeroBalanceException.NonZeroBalanceError.class, responseGetter);
NonZeroBalanceException exception =
new NonZeroBalanceException(
error.getMessage(), requestId, error.getCode(), statusCode, null);
diff --git a/src/main/java/com/stripe/exception/NotCancelableException.java b/src/main/java/com/stripe/exception/NotCancelableException.java
index d1a93541b2e..79e5e6027f1 100644
--- a/src/main/java/com/stripe/exception/NotCancelableException.java
+++ b/src/main/java/com/stripe/exception/NotCancelableException.java
@@ -18,9 +18,8 @@ private NotCancelableException(
static NotCancelableException parse(
JsonObject body, int statusCode, String requestId, StripeResponseGetter responseGetter) {
NotCancelableException.NotCancelableError error =
- (NotCancelableException.NotCancelableError)
- StripeObject.deserializeStripeObject(
- body, NotCancelableException.NotCancelableError.class, responseGetter);
+ StripeObject.deserializeStripeObject(
+ body, NotCancelableException.NotCancelableError.class, responseGetter);
NotCancelableException exception =
new NotCancelableException(
error.getMessage(), requestId, error.getCode(), statusCode, null);
diff --git a/src/main/java/com/stripe/exception/QuotaExceededException.java b/src/main/java/com/stripe/exception/QuotaExceededException.java
index 8428fe11abb..99f019e54a3 100644
--- a/src/main/java/com/stripe/exception/QuotaExceededException.java
+++ b/src/main/java/com/stripe/exception/QuotaExceededException.java
@@ -18,9 +18,8 @@ private QuotaExceededException(
static QuotaExceededException parse(
JsonObject body, int statusCode, String requestId, StripeResponseGetter responseGetter) {
QuotaExceededException.QuotaExceededError error =
- (QuotaExceededException.QuotaExceededError)
- StripeObject.deserializeStripeObject(
- body, QuotaExceededException.QuotaExceededError.class, responseGetter);
+ StripeObject.deserializeStripeObject(
+ body, QuotaExceededException.QuotaExceededError.class, responseGetter);
QuotaExceededException exception =
new QuotaExceededException(
error.getMessage(), requestId, error.getCode(), statusCode, null);
diff --git a/src/main/java/com/stripe/exception/RateLimitException.java b/src/main/java/com/stripe/exception/RateLimitException.java
index b8476ea4ee5..95b2fe6990b 100644
--- a/src/main/java/com/stripe/exception/RateLimitException.java
+++ b/src/main/java/com/stripe/exception/RateLimitException.java
@@ -26,9 +26,8 @@ public RateLimitException(
static RateLimitException parse(
JsonObject body, int statusCode, String requestId, StripeResponseGetter responseGetter) {
RateLimitException.RateLimitError error =
- (RateLimitException.RateLimitError)
- StripeObject.deserializeStripeObject(
- body, RateLimitException.RateLimitError.class, responseGetter);
+ StripeObject.deserializeStripeObject(
+ body, RateLimitException.RateLimitError.class, responseGetter);
RateLimitException exception =
new RateLimitException(
error.getMessage(), error.getParam(), requestId, error.getCode(), statusCode, null);
diff --git a/src/main/java/com/stripe/exception/RecipientNotNotifiableException.java b/src/main/java/com/stripe/exception/RecipientNotNotifiableException.java
index c6e7d2f6d93..5432d5caf95 100644
--- a/src/main/java/com/stripe/exception/RecipientNotNotifiableException.java
+++ b/src/main/java/com/stripe/exception/RecipientNotNotifiableException.java
@@ -21,11 +21,10 @@ private RecipientNotNotifiableException(
static RecipientNotNotifiableException parse(
JsonObject body, int statusCode, String requestId, StripeResponseGetter responseGetter) {
RecipientNotNotifiableException.RecipientNotNotifiableError error =
- (RecipientNotNotifiableException.RecipientNotNotifiableError)
- StripeObject.deserializeStripeObject(
- body,
- RecipientNotNotifiableException.RecipientNotNotifiableError.class,
- responseGetter);
+ StripeObject.deserializeStripeObject(
+ body,
+ RecipientNotNotifiableException.RecipientNotNotifiableError.class,
+ responseGetter);
RecipientNotNotifiableException exception =
new RecipientNotNotifiableException(
error.getMessage(), requestId, error.getCode(), statusCode, null);
diff --git a/src/main/java/com/stripe/exception/TemporarySessionExpiredException.java b/src/main/java/com/stripe/exception/TemporarySessionExpiredException.java
index 376c73a885b..917383cb01d 100644
--- a/src/main/java/com/stripe/exception/TemporarySessionExpiredException.java
+++ b/src/main/java/com/stripe/exception/TemporarySessionExpiredException.java
@@ -18,11 +18,10 @@ private TemporarySessionExpiredException(
static TemporarySessionExpiredException parse(
JsonObject body, int statusCode, String requestId, StripeResponseGetter responseGetter) {
TemporarySessionExpiredException.TemporarySessionExpiredError error =
- (TemporarySessionExpiredException.TemporarySessionExpiredError)
- StripeObject.deserializeStripeObject(
- body,
- TemporarySessionExpiredException.TemporarySessionExpiredError.class,
- responseGetter);
+ StripeObject.deserializeStripeObject(
+ body,
+ TemporarySessionExpiredException.TemporarySessionExpiredError.class,
+ responseGetter);
TemporarySessionExpiredException exception =
new TemporarySessionExpiredException(
error.getMessage(), requestId, error.getCode(), statusCode, null);
diff --git a/src/main/java/com/stripe/model/StripeObject.java b/src/main/java/com/stripe/model/StripeObject.java
index 68e4e7ed059..ad339c127aa 100644
--- a/src/main/java/com/stripe/model/StripeObject.java
+++ b/src/main/java/com/stripe/model/StripeObject.java
@@ -133,6 +133,12 @@ public static T deserializeStripeObject(
return (T) deserializeStripeObject(payload, (Type) type, responseGetter);
}
+ @SuppressWarnings("unchecked")
+ public static T deserializeStripeObject(
+ JsonObject payload, Class type, StripeResponseGetter responseGetter) {
+ return (T) deserializeStripeObject(payload, (Type) type, responseGetter);
+ }
+
public static StripeObject deserializeStripeObject(
String payload, Type type, StripeResponseGetter responseGetter) {
StripeObject object = ApiResource.INTERNAL_GSON.fromJson(payload, type);
diff --git a/src/main/java/com/stripe/net/Webhook.java b/src/main/java/com/stripe/net/Webhook.java
index 09505f70873..e5b8b5d0681 100644
--- a/src/main/java/com/stripe/net/Webhook.java
+++ b/src/main/java/com/stripe/net/Webhook.java
@@ -1,5 +1,6 @@
package com.stripe.net;
+import com.google.gson.JsonObject;
import com.stripe.exception.SignatureVerificationException;
import com.stripe.model.Event;
import com.stripe.model.StripeObject;
@@ -53,9 +54,10 @@ public static Event constructEvent(
}
/**
- * Returns an Event instance using the provided JSON payload. Throws a JsonSyntaxException if the
- * payload is not valid JSON, a SignatureVerificationException if the signature verification fails
- * for any reason, and an IllegalArgumentException if you pass the wrong type of input.
+ * Constructs a snapshot
+ * event from an incoming webhook after verifying its authenticity. To work with a webhook
+ * that has already been verified (i.e. one from a cloud provider, an asynchronous queue, or
+ * during testing), see {@code constructEventWithoutVerification}.
*
* @param payload the payload sent by Stripe.
* @param sigHeader the contents of the signature header sent by Stripe.
@@ -69,28 +71,90 @@ public static Event constructEvent(
public static Event constructEvent(
String payload, String sigHeader, String secret, long tolerance, Clock clock)
throws SignatureVerificationException {
- Event event =
- StripeObject.deserializeStripeObject(
- payload, Event.class, ApiResource.getGlobalResponseGetter());
+ Signature.verifyHeader(payload, sigHeader, secret, tolerance, clock);
+
+ return buildV1Event(payload);
+ }
+
+ /**
+ * Constructs a snapshot
+ * event from an incoming webhook without first verifying its authenticity. Should be used
+ * after calling {@code Webhook.Signature.verifyHeader(...)} or with input from a trusted source
+ * (such as AWS EventBridge,
+ * or Azure Event Grid
+ * payload). Or, to verify & construct in a single call, use {@code
+ * Webhook.constructEvent(...)} instead.
+ *
+ * @param payload the payload sent by Stripe, or a cloud provider envelope wrapping it.
+ * @return the Event instance
+ * @throws IllegalArgumentException if the payload is a v2 thin event notification.
+ */
+ public static Event constructEventWithoutVerification(String payload) {
+ return buildV1Event(maybeExtractFromCloudProviderEnvelope(payload));
+ }
- if ("v2.core.event".equals(event.getObject())) {
+ private static Event buildV1Event(String payload) {
+ return buildV1Event(ApiResource.GSON.fromJson(payload, JsonObject.class));
+ }
+
+ private static Event buildV1Event(JsonObject jsonObject) {
+ if (jsonObject.has("object")
+ && "v2.core.event".equals(jsonObject.get("object").getAsString())) {
throw new IllegalArgumentException(
- "You passed an event notification to Webhook.constructEvent, which expects a webhook payload."
- + " Use StripeClient.parseEventNotification instead.");
+ "You passed an event notification to Webhook method, which expects a webhook payload. Use the corresponding parseEventNotification method instead.");
}
- Signature.verifyHeader(payload, sigHeader, secret, tolerance, clock);
+ Event event =
+ StripeObject.deserializeStripeObject(
+ jsonObject, Event.class, ApiResource.getGlobalResponseGetter());
+
// StripeObjects source their raw JSON object from their last response, but constructed webhooks
// don't have that
// in order to make the raw object available on parsed events, we fake the response.
if (event.getLastResponse() == null) {
event.setLastResponse(
- new StripeResponse(200, HttpHeaders.of(Collections.emptyMap()), payload));
+ new StripeResponse(200, HttpHeaders.of(Collections.emptyMap()), jsonObject.toString()));
}
return event;
}
+ /**
+ * Parses a JSON payload (or cloud provider envelope) and returns the inner Stripe event JSON
+ * object. If the payload is already a raw Stripe event (object is "event" or "v2.core.event"), it
+ * is returned as-is. If it is an AWS EventBridge or Azure Event Grid envelope, the inner event is
+ * extracted. Throws {@link IllegalArgumentException} for unrecognized formats.
+ *
+ * @param payload the raw JSON string.
+ * @return the inner event as a {@link JsonObject}.
+ */
+ public static JsonObject maybeExtractFromCloudProviderEnvelope(String payload) {
+ JsonObject root = ApiResource.GSON.fromJson(payload, JsonObject.class);
+
+ // AWS
+ // https://docs.stripe.com/event-destinations/eventbridge#event-structure
+ if (root.has("detail")) {
+ return root.get("detail").getAsJsonObject();
+ }
+
+ // Azure
+ // https://docs.stripe.com/event-destinations/eventgrid#event-structure
+ if (root.has("specversion") && root.has("data")) {
+ return root.get("data").getAsJsonObject();
+ }
+
+ // Raw Stripe event passed directly: pass through as-is
+ if (root.has("object") && root.get("object").isJsonPrimitive()) {
+ String object = root.get("object").getAsString();
+ if ("event".equals(object) || "v2.core.event".equals(object)) {
+ return root;
+ }
+ }
+
+ throw new IllegalArgumentException(
+ "Unrecognized event format. The payload must be an AWS EventBridge/Azure Event Grid event envelope or a Stripe webhook (thin event notification or snapshot).");
+ }
+
public static final class Signature {
public static final String EXPECTED_SCHEME = "v1";
@@ -112,8 +176,10 @@ public static boolean verifyHeader(
}
/**
- * Verifies the signature header sent by Stripe. Throws a SignatureVerificationException if the
- * verification fails for any reason.
+ * Verifies the authenticity (and recency) of a webhook, throwing a {@code
+ * SignatureVerificationException} if there's a mismatch. Useful for quickly validating incoming
+ * webhooks before storing them for later processing (at which time you can use the {@code
+ * *WithoutVerification} methods for parsing).
*
* @param payload the payload sent by Stripe.
* @param sigHeader the contents of the signature header sent by Stripe.
@@ -171,6 +237,35 @@ public static boolean verifyHeader(
return true;
}
+ /**
+ * Generates a {@code Stripe-Signature} header for the given payload and secret using the
+ * current timestamp.
+ *
+ * @param payload the payload to sign.
+ * @param secret the webhook secret.
+ * @return the generated signature header string.
+ */
+ public static String generateSignatureHeader(String payload, String secret)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ return generateSignatureHeader(payload, secret, Util.getTimeNow());
+ }
+
+ /**
+ * Compute the {@code Stripe-Signature} header for a given webhook body & secret. Useful for
+ * signing payloads in unit tests.
+ *
+ * @param payload the payload to sign.
+ * @param secret the webhook secret.
+ * @param timestamp the timestamp to use (seconds since epoch).
+ * @return the generated signature header string.
+ */
+ public static String generateSignatureHeader(String payload, String secret, long timestamp)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ String payloadToSign = String.format("%d.%s", timestamp, payload);
+ String signature = computeSignature(payloadToSign, secret);
+ return String.format("t=%d,%s=%s", timestamp, EXPECTED_SCHEME, signature);
+ }
+
/**
* Extracts the timestamp in a signature header.
*
diff --git a/src/test/java/com/stripe/StripeClientTest.java b/src/test/java/com/stripe/StripeClientTest.java
index aa97216e5a4..d53110efe61 100644
--- a/src/test/java/com/stripe/StripeClientTest.java
+++ b/src/test/java/com/stripe/StripeClientTest.java
@@ -246,6 +246,7 @@ public void parsesEventNotificationWithRelatedObject()
client.parseEventNotification(v2EventNotificationWithRelatedObject, signature, secret);
assertNotNull(eventNotification);
assertEquals("evt_234", eventNotification.getId());
+ assertEquals("v2.core.event", eventNotification.getObject());
assertEquals("v1.billing.meter.error_report_triggered", eventNotification.getType());
assertEquals(Instant.parse("2022-02-15T00:27:45.330Z"), eventNotification.created);
assertEquals("org_123", eventNotification.getContext().toString());
diff --git a/src/test/java/com/stripe/net/CloudProviderEventTest.java b/src/test/java/com/stripe/net/CloudProviderEventTest.java
new file mode 100644
index 00000000000..11234e560df
--- /dev/null
+++ b/src/test/java/com/stripe/net/CloudProviderEventTest.java
@@ -0,0 +1,236 @@
+package com.stripe.net;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSyntaxException;
+import com.stripe.BaseStripeTest;
+import com.stripe.StripeClient;
+import com.stripe.model.Event;
+import com.stripe.model.v2.core.EventNotification;
+import org.junit.jupiter.api.Test;
+
+public class CloudProviderEventTest extends BaseStripeTest {
+
+ private static final String EVENTBRIDGE_PAYLOAD =
+ "{\"version\":\"0\",\"id\":\"17e8dff5-d6cd-3770-ace9-aeac02b6ac3f\","
+ + "\"detail-type\":\"customer.created\","
+ + "\"source\":\"aws.partner/stripe.com/ed_123\","
+ + "\"account\":\"506417113029\","
+ + "\"time\":\"2024-03-07T18:27:56Z\","
+ + "\"region\":\"us-west-2\","
+ + "\"resources\":[],"
+ + "\"detail\":{"
+ + "\"id\":\"evt_test_123\","
+ + "\"object\":\"event\","
+ + "\"api_version\":\"2023-10-16\","
+ + "\"created\":1709836076,"
+ + "\"data\":{\"object\":{\"id\":\"cus_123\",\"object\":\"customer\"}},"
+ + "\"livemode\":true,"
+ + "\"pending_webhooks\":0,"
+ + "\"request\":{\"id\":\"req_123\",\"idempotency_key\":null},"
+ + "\"type\":\"customer.created\"}}";
+
+ private static final String EVENTGRID_PAYLOAD =
+ "{\"specversion\":\"1.0\","
+ + "\"type\":\"customer.created\","
+ + "\"source\":\"/providers/stripe/ed_test_123\","
+ + "\"id\":\"9aeb0fdf-c01e-0131-0922-9eb54906e209\","
+ + "\"time\":\"2025-07-11T14:30:00Z\","
+ + "\"subject\":null,"
+ + "\"dataContentType\":\"application/cloudevents+json\","
+ + "\"data\":{"
+ + "\"id\":\"evt_test_456\","
+ + "\"object\":\"event\","
+ + "\"api_version\":\"2023-10-16\","
+ + "\"created\":1709836076,"
+ + "\"data\":{\"object\":{\"id\":\"cus_456\",\"object\":\"customer\"}},"
+ + "\"livemode\":false,"
+ + "\"pending_webhooks\":0,"
+ + "\"request\":{\"id\":\"req_456\",\"idempotency_key\":null},"
+ + "\"type\":\"customer.created\"}}";
+
+ private static final String EVENTBRIDGE_NOTIFICATION_PAYLOAD =
+ "{\"version\":\"0\",\"id\":\"17e8dff5-d6cd-3770-ace9-aeac02b6ac3f\","
+ + "\"detail-type\":\"v2.core.event_destination.ping\","
+ + "\"source\":\"aws.partner/stripe.com/ed_123\","
+ + "\"detail\":{"
+ + "\"id\":\"evt_test_789\","
+ + "\"object\":\"v2.core.event\","
+ + "\"type\":\"v2.core.event_destination.ping\","
+ + "\"created\":\"2024-03-07T18:27:56.000Z\","
+ + "\"livemode\":true}}";
+
+ private static final String EVENTGRID_NOTIFICATION_PAYLOAD =
+ "{\"specversion\":\"1.0\","
+ + "\"type\":\"v2.core.event_destination.ping\","
+ + "\"source\":\"/providers/stripe/ed_test_123\","
+ + "\"id\":\"9aeb0fdf-c01e-0131-0922-9eb54906e209\","
+ + "\"data\":{"
+ + "\"id\":\"evt_test_789\","
+ + "\"object\":\"v2.core.event\","
+ + "\"type\":\"v2.core.event_destination.ping\","
+ + "\"created\":\"2024-03-07T18:27:56.000Z\","
+ + "\"livemode\":true}}";
+
+ private static final String RAW_EVENT_PAYLOAD =
+ "{\"id\":\"evt_test_123\","
+ + "\"object\":\"event\","
+ + "\"api_version\":\"2023-10-16\","
+ + "\"created\":1709836076,"
+ + "\"data\":{\"object\":{\"id\":\"cus_123\",\"object\":\"customer\"}},"
+ + "\"livemode\":true,"
+ + "\"pending_webhooks\":0,"
+ + "\"request\":{\"id\":\"req_123\",\"idempotency_key\":null},"
+ + "\"type\":\"customer.created\"}";
+
+ // constructEventWithoutVerification tests
+
+ @Test
+ public void testEventBridgeViaClient() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ Event event = client.constructEventWithoutVerification(EVENTBRIDGE_PAYLOAD);
+ assertNotNull(event);
+ assertEquals("evt_test_123", event.getId());
+ assertEquals("customer.created", event.getType());
+ }
+
+ @Test
+ public void testEventGridViaClient() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ Event event = client.constructEventWithoutVerification(EVENTGRID_PAYLOAD);
+ assertNotNull(event);
+ assertEquals("evt_test_456", event.getId());
+ assertEquals("customer.created", event.getType());
+ }
+
+ @Test
+ public void testRawEventViaClient() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ Event event = client.constructEventWithoutVerification(RAW_EVENT_PAYLOAD);
+ assertNotNull(event);
+ assertEquals("evt_test_123", event.getId());
+ assertEquals("customer.created", event.getType());
+ }
+
+ @Test
+ public void testInvalidJsonViaClient() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ assertThrows(
+ JsonSyntaxException.class,
+ () -> client.constructEventWithoutVerification("not valid json"));
+ }
+
+ @Test
+ public void testUnrecognizedFormatViaClient() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> client.constructEventWithoutVerification("{\"foo\":\"bar\"}"));
+ assertTrue(ex.getMessage().contains("Unrecognized event format"));
+ }
+
+ // parseEventNotificationWithoutVerification tests
+
+ @Test
+ public void testEventBridgeNotificationViaClient() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ EventNotification notification =
+ client.parseEventNotificationWithoutVerification(EVENTBRIDGE_NOTIFICATION_PAYLOAD);
+ assertNotNull(notification);
+ assertEquals("evt_test_789", notification.getId());
+ }
+
+ @Test
+ public void testEventGridNotificationViaClient() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ EventNotification notification =
+ client.parseEventNotificationWithoutVerification(EVENTGRID_NOTIFICATION_PAYLOAD);
+ assertNotNull(notification);
+ assertEquals("evt_test_789", notification.getId());
+ }
+
+ @Test
+ public void testParseNotificationWithV1EventSuggestsConstructEventWithoutVerification() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> client.parseEventNotificationWithoutVerification(EVENTBRIDGE_PAYLOAD));
+ }
+
+ @Test
+ public void testParseNotificationInvalidJsonThrows() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ assertThrows(
+ JsonSyntaxException.class,
+ () -> client.parseEventNotificationWithoutVerification("not valid json"));
+ }
+
+ @Test
+ public void testParseNotificationUnrecognizedFormatThrows() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> client.parseEventNotificationWithoutVerification("{\"foo\":\"bar\"}"));
+ assertTrue(ex.getMessage().contains("Unrecognized event format"));
+ }
+
+ @Test
+ public void testParseNotificationRawV2NotificationPassthrough() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ String rawV2Payload =
+ "{\"id\":\"evt_234\","
+ + "\"object\":\"v2.core.event\","
+ + "\"type\":\"v2.core.event_destination.ping\","
+ + "\"created\":\"2024-03-07T18:27:56.000Z\","
+ + "\"livemode\":true}";
+ EventNotification notification = client.parseEventNotificationWithoutVerification(rawV2Payload);
+ assertNotNull(notification);
+ assertEquals("evt_234", notification.getId());
+ }
+
+ @Test
+ public void testConstructEventWithoutVerificationRejectsV2ThinEvent() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ String v2Payload =
+ "{\"id\":\"evt_234\","
+ + "\"object\":\"v2.core.event\","
+ + "\"type\":\"v2.core.event_destination.ping\","
+ + "\"created\":\"2024-03-07T18:27:56.000Z\","
+ + "\"livemode\":true}";
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> client.constructEventWithoutVerification(v2Payload));
+ assertTrue(ex.getMessage().contains("parseEventNotification"));
+ }
+
+ @Test
+ public void testAzureEnvelopeMissingDataThrows() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ String payloadMissingData =
+ "{\"specversion\":\"1.0\","
+ + "\"type\":\"customer.created\","
+ + "\"source\":\"/providers/stripe/ed_test_123\","
+ + "\"id\":\"test-missing-data\"}";
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> client.constructEventWithoutVerification(payloadMissingData));
+ assertTrue(ex.getMessage().contains("Unrecognized event format"));
+ }
+
+ @Test
+ public void testFromJsonUnexpectedObjectTypeThrows() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("object", "customer");
+ jsonObject.addProperty("type", "customer.created");
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class, () -> EventNotification.fromJson(jsonObject, client));
+ assertTrue(ex.getMessage().contains("Unexpected object type"));
+ }
+}
diff --git a/src/test/java/com/stripe/net/WebhookTest.java b/src/test/java/com/stripe/net/WebhookTest.java
index 3e1357b6bf3..cf7a7c7d9c7 100644
--- a/src/test/java/com/stripe/net/WebhookTest.java
+++ b/src/test/java/com/stripe/net/WebhookTest.java
@@ -5,6 +5,7 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import com.stripe.BaseStripeTest;
import com.stripe.Stripe;
@@ -76,7 +77,7 @@ public static String generateSigHeader(Map options)
@Test
public void testValidJsonAndHeader()
throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
- final String sigHeader = generateSigHeader();
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret);
final Event event = Webhook.constructEvent(payload, sigHeader, secret);
@@ -86,10 +87,7 @@ public void testValidJsonAndHeader()
@Test
public void testValidJsonAndHeaderButOutsideTimeTolerance()
throws NoSuchAlgorithmException, InvalidKeyException {
- final Map options = new HashMap<>();
- options.put("timestamp", 1L);
-
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret, 1L);
final Clock clock = Clock.fixed(Instant.ofEpochMilli(12000), ZoneId.of("UTC"));
assertThrows(
@@ -109,9 +107,7 @@ public void testValidJsonAndHeaderCanMakeRequestsOnDataObject()
+ "\","
+ "\"object\": \"event\",\"data\": {\"object\": {\"id\": \"acct_123\",\"object\": \"account\"}}}";
- final Map options = new HashMap<>();
- options.put("payload", payload);
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret);
final Event event = Webhook.constructEvent(payload, sigHeader, secret);
Account modelViaData = ((Account) event.getData().getObject());
@@ -125,9 +121,7 @@ public void testValidJsonAndHeaderCanMakeRequestsOnDataObject()
public void testInvalidJson()
throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
final String payload = "this is not valid JSON";
- final Map options = new HashMap<>();
- options.put("payload", payload);
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret);
assertThrows(
JsonSyntaxException.class,
@@ -196,9 +190,8 @@ public void testNoValidSignatureForPayload()
@Test
public void testTimestampOutsideTolerance()
throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
- final Map options = new HashMap<>();
- options.put("timestamp", Webhook.Util.getTimeNow() - 15);
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader =
+ Webhook.Signature.generateSignatureHeader(payload, secret, Webhook.Util.getTimeNow() - 15);
Throwable exception =
assertThrows(
@@ -212,7 +205,7 @@ public void testTimestampOutsideTolerance()
@Test
public void testValidHeaderAndSignature()
throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
- final String sigHeader = generateSigHeader();
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret);
assertTrue(Webhook.Signature.verifyHeader(payload, sigHeader, secret, 10, null));
}
@@ -220,7 +213,9 @@ public void testValidHeaderAndSignature()
@Test
public void testHeaderContainsValidSignature()
throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
- final String sigHeader = String.format("%s,v1=bad_signature", generateSigHeader());
+ final String sigHeader =
+ String.format(
+ "%s,v1=bad_signature", Webhook.Signature.generateSignatureHeader(payload, secret));
assertTrue(Webhook.Signature.verifyHeader(payload, sigHeader, secret, 10, null));
}
@@ -228,9 +223,7 @@ public void testHeaderContainsValidSignature()
@Test
public void testTimestampOffButNoTolerance()
throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
- final Map options = new HashMap<>();
- options.put("timestamp", Long.valueOf(12345L));
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret, 12345L);
assertTrue(Webhook.Signature.verifyHeader(payload, sigHeader, secret, 0, null));
}
@@ -238,11 +231,7 @@ public void testTimestampOffButNoTolerance()
@Test
public void testTimestampWithClock()
throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
-
- final Map options = new HashMap<>();
- options.put("timestamp", 11L);
-
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret, 11L);
final Clock clock = Clock.fixed(Instant.ofEpochMilli(1), ZoneId.of("UTC"));
assertTrue(Webhook.Signature.verifyHeader(payload, sigHeader, secret, 10, clock));
@@ -251,11 +240,7 @@ public void testTimestampWithClock()
@Test
public void testTimestampWithClockOutsideTolerance()
throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
-
- final Map options = new HashMap<>();
- options.put("timestamp", 11L);
-
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret, 11L);
final Clock clock = Clock.fixed(Instant.ofEpochMilli(12), ZoneId.of("UTC"));
assertTrue(Webhook.Signature.verifyHeader(payload, sigHeader, secret, 10, clock));
@@ -282,9 +267,7 @@ public void testStripeClientConstructEvent()
+ "\","
+ "\"object\": \"event\",\"data\": {\"object\": {\"id\": \"rdr_123\",\"object\": \"terminal.reader\"}}}";
- final Map options = new HashMap<>();
- options.put("payload", payload);
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret);
final Event event = client.constructEvent(payload, sigHeader, secret);
@@ -315,9 +298,7 @@ public void testStripeClientConstructEventWithTolerance()
+ "\","
+ "\"object\": \"event\",\"data\": {\"object\": {\"id\": \"rdr_123\",\"object\": \"terminal.reader\"}}}";
- final Map options = new HashMap<>();
- options.put("payload", payload);
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret);
final Event event = client.constructEvent(payload, sigHeader, secret, 500);
@@ -331,19 +312,116 @@ public void testStripeClientConstructEventWithTolerance()
public void testConstructEventWithRawJson()
throws StripeException, NoSuchAlgorithmException, InvalidKeyException {
- final Event event = Webhook.constructEvent(payload, generateSigHeader(), secret);
+ final Event event =
+ Webhook.constructEvent(
+ payload, Webhook.Signature.generateSignatureHeader(payload, secret), secret);
assertNotNull(event.getRawJsonObject());
}
+ @Test
+ public void testGenerateSignatureHeaderWithTimestamp()
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureVerificationException {
+ final long timestamp = 1609459200L;
+ final String header = Webhook.Signature.generateSignatureHeader(payload, secret, timestamp);
+
+ assertTrue(Webhook.Signature.verifyHeader(payload, header, secret, 0, null));
+ assertTrue(header.startsWith(String.format("t=%d,v1=", timestamp)));
+ }
+
+ @Test
+ public void testGenerateSignatureHeaderWithCurrentTimestamp()
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureVerificationException {
+ final String header = Webhook.Signature.generateSignatureHeader(payload, secret);
+
+ assertTrue(Webhook.Signature.verifyHeader(payload, header, secret, 10, null));
+ }
+
+ @Test
+ public void testGenerateSignatureHeaderRoundtrip()
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureVerificationException {
+ String payload = "test_payload";
+ String secret = "whsec_test_secret";
+ String header = Webhook.Signature.generateSignatureHeader(payload, secret);
+ // Should not throw:
+ assertTrue(Webhook.Signature.verifyHeader(payload, header, secret, 300));
+ }
+
+ @Test
+ public void testParseEventNotification()
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureVerificationException {
+ StripeClient client = new StripeClient(new LiveStripeResponseGetter());
+
+ final String v2Payload =
+ "{\n"
+ + " \"id\": \"evt_test_webhook\",\n"
+ + " \"object\": \"v2.core.event\",\n"
+ + " \"type\": \"v1.billing.meter.no_meter_found\",\n"
+ + " \"livemode\": false,\n"
+ + " \"created\": \"2022-02-15T00:27:45.330Z\"\n"
+ + "}";
+
+ final String header = Webhook.Signature.generateSignatureHeader(v2Payload, secret);
+
+ final com.stripe.model.v2.core.EventNotification notification =
+ client.parseEventNotification(v2Payload, header, secret);
+
+ assertNotNull(notification);
+ assertEquals("evt_test_webhook", notification.getId());
+ }
+
+ @Test
+ public void testMaybeExtractPassesThroughV1Event() {
+ final String v1Payload = "{\"id\": \"evt_test_webhook\", \"object\": \"event\"}";
+ final JsonObject result = Webhook.maybeExtractFromCloudProviderEnvelope(v1Payload);
+ assertEquals("event", result.get("object").getAsString());
+ }
+
+ @Test
+ public void testMaybeExtractPassesThroughV2Event() {
+ final String v2Payload = "{\"id\": \"evt_test_webhook\", \"object\": \"v2.core.event\"}";
+ final JsonObject result = Webhook.maybeExtractFromCloudProviderEnvelope(v2Payload);
+ assertEquals("v2.core.event", result.get("object").getAsString());
+ }
+
+ @Test
+ public void testMaybeExtractThrowsForUnrecognizedFormat() {
+ final String unknownPayload = "{\"id\": \"evt_test_webhook\", \"object\": \"something_else\"}";
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ Webhook.maybeExtractFromCloudProviderEnvelope(unknownPayload);
+ });
+ }
+
+ @Test
+ public void testConstructEventWithoutVerification() {
+ final Event event = Webhook.constructEventWithoutVerification(payload);
+
+ assertNotNull(event);
+ assertEquals("evt_test_webhook", event.getId());
+ }
+
+ @Test
+ public void testConstructEventWithoutVerificationRejectsV2Payload() {
+ final String v2Payload =
+ "{\n \"id\": \"evt_test_webhook\",\n \"object\": \"v2.core.event\"\n}";
+
+ Throwable exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ Webhook.constructEventWithoutVerification(v2Payload);
+ });
+ assertTrue(exception.getMessage().contains("parseEventNotification"));
+ }
+
@Test
public void testConstructEventRejectsV2Payload()
throws NoSuchAlgorithmException, InvalidKeyException {
final String v2Payload =
"{\n \"id\": \"evt_test_webhook\",\n \"object\": \"v2.core.event\"\n}";
- final Map options = new HashMap<>();
- options.put("payload", v2Payload);
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(v2Payload, secret);
Throwable exception =
assertThrows(
@@ -351,7 +429,7 @@ public void testConstructEventRejectsV2Payload()
() -> {
Webhook.constructEvent(v2Payload, sigHeader, secret);
});
- assertTrue(exception.getMessage().contains("StripeClient.parseEventNotification"));
+ assertTrue(exception.getMessage().contains("parseEventNotification"));
}
@Test
@@ -361,9 +439,7 @@ public void testClientConstructEventRejectsV2Payload()
final String v2Payload =
"{\n \"id\": \"evt_test_webhook\",\n \"object\": \"v2.core.event\"\n}";
- final Map options = new HashMap<>();
- options.put("payload", v2Payload);
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(v2Payload, secret);
Throwable exception =
assertThrows(
@@ -371,6 +447,6 @@ public void testClientConstructEventRejectsV2Payload()
() -> {
client.constructEvent(v2Payload, sigHeader, secret);
});
- assertTrue(exception.getMessage().contains("StripeClient.parseEventNotification"));
+ assertTrue(exception.getMessage().contains("parseEventNotification"));
}
}