Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CODEGEN_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
e83a2c042b6fd289e2c574f1db817b0aa55d8b0d
baff58c9d515cdd5f5c3231d101989d588788c6f
2 changes: 1 addition & 1 deletion OPENAPI_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2413
v2442
102 changes: 102 additions & 0 deletions examples/async_event_notification_handler_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""
async_event_notification_handler_endpoint.py - receive and process event notifications (AKA thin events) like "v1.billing.meter.error_report_triggered" using AsyncEventNotificationHandler.

The async equivalent of event_notification_handler_endpoint.py. In this example, we:
- write an async fallback callback to handle unrecognized event notifications
- create a StripeClient called client
- Initialize an AsyncStripeEventNotificationHandler with the client, webhook secret, and fallback callback
- register a pre_handle hook that deduplicates events by id before any callback runs
- register a specific handler for the "v1.billing.meter.error_report_triggered" event notification type
- await handler.handle_async() to process the received notification webhook body

Note that only your callbacks are awaited. Verifying the signature and parsing the
payload are pure CPU work, so they stay synchronous even here.
"""

import os
from fastapi import FastAPI, Request, Response

from stripe import StripeClient, UnhandledNotificationDetails
from stripe.v2.core import EventNotification
from stripe.events import V1BillingMeterErrorReportTriggeredEventNotification

app = FastAPI()
api_key = os.environ.get("STRIPE_API_KEY", "")
webhook_secret = os.environ.get("WEBHOOK_SECRET", "")

# Webhooks can be delivered more than once, so we track ids we've already
# processed. In production, back this with something durable and shared
# across processes (e.g. Redis or a database table) instead of an in-memory set.
processed_event_ids: set[str] = set()


async def fallback_callback(
notif: EventNotification,
client: StripeClient,
details: UnhandledNotificationDetails,
):
print(f"Got an unhandled event of type {notif.type}!")


client = StripeClient(api_key)
handler = client.async_notification_handler(webhook_secret, fallback_callback)

# Handles events delivered through a channel that has already authenticated them, such as
# AWS EventBridge or Azure Event Grid. Those payloads carry no Stripe-Signature header.
unverified_handler = client.async_notification_handler_without_verification(
fallback_callback
)


@handler.pre_handle
@unverified_handler.pre_handle
async def deduplicate_events(
notif: EventNotification, client: StripeClient
) -> bool:
"""
Runs before any registered callback. Returning False
here skips handling entirely for this delivery, which is useful for
deduplicating webhooks.
"""
if notif.id in processed_event_ids:
print(f"Already processed {notif.id}, skipping.")
return False

processed_event_ids.add(notif.id)
return True


# can be anywhere in your codebase; registering on both handlers means either
# endpoint below will route this event type
@handler.on_v1_billing_meter_error_report_triggered
@unverified_handler.on_v1_billing_meter_error_report_triggered
async def handle_meter_error(
notif: V1BillingMeterErrorReportTriggeredEventNotification,
client: StripeClient,
):
# the async variants of the fetch methods keep the whole callback non-blocking
event = await notif.fetch_event_async()
print(f"Err! No meter found: {event.data.developer_message_summary}")


@app.post("/webhook")
async def webhook(request: Request):
webhook_body = await request.body()
sig_header = request.headers.get("Stripe-Signature", "")

try:
await handler.handle_async(webhook_body.decode(), sig_header)
return Response(status_code=200)
except Exception as e:
return Response(content=str(e), status_code=500)


@app.post("/webhook-from-cloud-provider")
async def webhook_from_cloud_provider(request: Request):
# no signature header to pass along; the channel already authenticated this event
try:
body = await request.body()
await unverified_handler.handle_async(body)
return Response(status_code=200)
except Exception as e:
return Response(content=str(e), status_code=500)
27 changes: 24 additions & 3 deletions examples/event_notification_handler_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- write a fallback callback to handle unrecognized event notifications
- create a StripeClient called client
- Initialize an EventNotificationHandler with the client, webhook secret, and fallback callback
- register a pre_handle hook that deduplicates events by id before any callback runs
- register a specific handler for the "v1.billing.meter.error_report_triggered" event notification type
- use handler.handle() to process the received notification webhook body
"""
Expand All @@ -20,6 +21,11 @@
api_key = os.environ.get("STRIPE_API_KEY", "")
webhook_secret = os.environ.get("WEBHOOK_SECRET", "")

# Webhooks can be delivered more than once, so we track ids we've already
# processed. In production, back this with something durable and shared
# across processes (e.g. Redis or a database table) instead of an in-memory set.
processed_event_ids: set[str] = set()


def fallback_callback(
notif: EventNotification,
Expand All @@ -39,6 +45,22 @@ def fallback_callback(
)


@handler.pre_handle
@unverified_handler.pre_handle
def deduplicate_events(notif: EventNotification, client: StripeClient) -> bool:
"""
Runs before any registered callback. Returning False
here skips handling entirely for this delivery, which is useful for
deduplicating webhooks.
"""
if notif.id in processed_event_ids:
print(f"Already processed {notif.id}, skipping.")
return False

processed_event_ids.add(notif.id)
return True


# can be anywhere in your codebase; registering on both handlers means either
# endpoint below will route this event type
@handler.on_v1_billing_meter_error_report_triggered
Expand All @@ -53,10 +75,9 @@ def handle_meter_error(

@app.route("/webhook", methods=["POST"])
def webhook():
webhook_body = request.data
sig_header = request.headers.get("Stripe-Signature")

try:
webhook_body = request.data
sig_header = request.headers.get("Stripe-Signature")
handler.handle(webhook_body, sig_header)
return jsonify(success=True), 200
except Exception as e:
Expand Down
6 changes: 3 additions & 3 deletions examples/event_notification_webhook_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@

@app.route("/webhook", methods=["POST"])
def webhook():
webhook_body = request.data
sig_header = request.headers.get("Stripe-Signature")

try:
webhook_body = request.data
sig_header = request.headers.get("Stripe-Signature")

event_notif = client.parse_event_notification(
webhook_body, sig_header, webhook_secret
)
Expand Down
2 changes: 1 addition & 1 deletion examples/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

@app.route("/webhooks", methods=["POST"])
def webhooks():
payload = request.data.decode("utf-8")
payload = request.data
received_sig = request.headers.get("Stripe-Signature", None)

try:
Expand Down
21 changes: 21 additions & 0 deletions stripe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,12 @@ def add_beta_version(
from stripe._customer_session_service import (
CustomerSessionService as CustomerSessionService,
)
from stripe._customer_tax_exemption import (
CustomerTaxExemption as CustomerTaxExemption,
)
from stripe._customer_tax_exemption_service import (
CustomerTaxExemptionService as CustomerTaxExemptionService,
)
from stripe._customer_tax_id_service import (
CustomerTaxIdService as CustomerTaxIdService,
)
Expand Down Expand Up @@ -375,6 +381,8 @@ def add_beta_version(
)
from stripe._event import Event as Event
from stripe._event_notification_handler import (
AsyncStripeEventNotificationHandler as AsyncStripeEventNotificationHandler,
AsyncStripeEventNotificationHandlerWithoutVerification as AsyncStripeEventNotificationHandlerWithoutVerification,
StripeEventNotificationHandler as StripeEventNotificationHandler,
StripeEventNotificationHandlerWithoutVerification as StripeEventNotificationHandlerWithoutVerification,
UnhandledNotificationDetails as UnhandledNotificationDetails,
Expand Down Expand Up @@ -830,6 +838,11 @@ def add_beta_version(
"CustomerService": ("stripe._customer_service", False),
"CustomerSession": ("stripe._customer_session", False),
"CustomerSessionService": ("stripe._customer_session_service", False),
"CustomerTaxExemption": ("stripe._customer_tax_exemption", False),
"CustomerTaxExemptionService": (
"stripe._customer_tax_exemption_service",
False,
),
"CustomerTaxIdService": ("stripe._customer_tax_id_service", False),
"DelegatedCheckoutService": ("stripe._delegated_checkout_service", False),
"DeletableAPIResource": ("stripe._deletable_api_resource", False),
Expand Down Expand Up @@ -871,6 +884,14 @@ def add_beta_version(
"ErrorObject": ("stripe._error_object", False),
"OAuthErrorObject": ("stripe._error_object", False),
"Event": ("stripe._event", False),
"AsyncStripeEventNotificationHandler": (
"stripe._event_notification_handler",
False,
),
"AsyncStripeEventNotificationHandlerWithoutVerification": (
"stripe._event_notification_handler",
False,
),
"StripeEventNotificationHandler": (
"stripe._event_notification_handler",
False,
Expand Down
22 changes: 16 additions & 6 deletions stripe/_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,12 +597,6 @@ class Capabilities(StripeObject):
"""
The status of the SEPA Direct Debits payments capability of the account, or whether the account can directly process SEPA Direct Debits charges.
"""
sequra_payments: Optional[
Union[Literal["active", "inactive", "pending"], str]
]
"""
The status of the SeQura capability of the account, or whether the account can directly process SeQura payments.
"""
shopeepay_payments: Optional[
Union[Literal["active", "inactive", "pending"], str]
]
Expand Down Expand Up @@ -1144,6 +1138,12 @@ class Alternative(StripeObject):
"""

class Error(StripeObject):
class Details(StripeObject):
partner_rejection_code: Optional[str]
"""
The rejection code as received from our payment method partner.
"""

code: Literal[
"external_request",
"information_missing",
Expand Down Expand Up @@ -1248,6 +1248,7 @@ class Error(StripeObject):
"""
The code for the type of error.
"""
details: Optional[Details]
reason: str
"""
An informative message that indicates the error type and provides additional details about the error.
Expand All @@ -1256,6 +1257,7 @@ class Error(StripeObject):
"""
The specific user onboarding requirement field (in the requirements hash) that needs to be resolved.
"""
_inner_class_types = {"details": Details}

alternatives: Optional[List[Alternative]]
"""
Expand Down Expand Up @@ -1327,6 +1329,12 @@ class Alternative(StripeObject):
"""

class Error(StripeObject):
class Details(StripeObject):
partner_rejection_code: Optional[str]
"""
The rejection code as received from our payment method partner.
"""

code: Literal[
"external_request",
"information_missing",
Expand Down Expand Up @@ -1431,6 +1439,7 @@ class Error(StripeObject):
"""
The code for the type of error.
"""
details: Optional[Details]
reason: str
"""
An informative message that indicates the error type and provides additional details about the error.
Expand All @@ -1439,6 +1448,7 @@ class Error(StripeObject):
"""
The specific user onboarding requirement field (in the requirements hash) that needs to be resolved.
"""
_inner_class_types = {"details": Details}

alternatives: Optional[List[Alternative]]
"""
Expand Down
16 changes: 16 additions & 0 deletions stripe/_account_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,20 @@ class Features(StripeObject):
features: Features
_inner_class_types = {"features": Features}

class PaymentMethodSettings(StripeObject):
class Features(StripeObject):
disable_stripe_user_authentication: bool
"""
Whether Stripe user authentication is disabled. This value can only be `true` for accounts where `controller.requirement_collection` is `application` for the account. This is `false` by default.
"""

enabled: bool
"""
Whether the embedded component is enabled.
"""
features: Features
_inner_class_types = {"features": Features}

class Payments(StripeObject):
class Features(StripeObject):
capture_payments: bool
Expand Down Expand Up @@ -715,6 +729,7 @@ class Features(StripeObject):
notification_banner: NotificationBanner
payment_details: PaymentDetails
payment_disputes: PaymentDisputes
payment_method_settings: PaymentMethodSettings
payments: Payments
payout_details: PayoutDetails
payout_reconciliation_report: PayoutReconciliationReport
Expand Down Expand Up @@ -760,6 +775,7 @@ class Features(StripeObject):
"notification_banner": NotificationBanner,
"payment_details": PaymentDetails,
"payment_disputes": PaymentDisputes,
"payment_method_settings": PaymentMethodSettings,
"payments": Payments,
"payout_details": PayoutDetails,
"payout_reconciliation_report": PayoutReconciliationReport,
Expand Down
2 changes: 1 addition & 1 deletion stripe/_api_version.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# File generated from our OpenAPI spec
class _ApiVersion:
CURRENT = "2026-08-12.preview"
CURRENT = "2026-08-26.preview"
CURRENT_MAJOR = ""
Loading
Loading