stripe
stripe is Stripe's official Python SDK for its payments API: charges, subscriptions, Checkout, Connect, invoicing, and everything else the platform does. Since v8 the intended entry point is the StripeClient class, where every call goes through a service tree like client.v1.payment_intents.create(params={...}) and parameters travel in an explicit params dict. The older module-level style (stripe.api_key plus stripe.PaymentIntent.create(...)) still works but is slated for deprecation, and once that lands, new endpoints will only appear on StripeClient. The library ships full type annotations, automatic retries with generated idempotency keys, webhook signature verification, auto-paginating list iterators, and async variants of every request method via an _async suffix.
If you take payments with Stripe from Python, use it; it is well typed, actively maintained, and the webhook and retry plumbing alone justifies it. Just write new code against StripeClient, pin a minor range, and budget for regular major-version upgrades.
Use it if
- You are integrating Stripe payments from Python at all: it is the official SDK, it tracks every API change within days, and hand-rolling signature verification or pagination against the raw REST API buys you nothing
- You want typed request and response objects: since v7.1.0 every endpoint has inline annotations (TypedDict params, typed resources) that Pyright checks well
- You run an async stack: every request method has an _async twin, and HTTPXClient or AIOHTTPClient plug straight into StripeClient (pip install stripe[async] pulls the dependency since v13.0.1)
- You need webhook handling done right: client.construct_event verifies the signature with a 300-second replay tolerance and hands back a parsed Event in one call
- You expect a stable major version: the SDK went from v8 to v15 in roughly two years because majors track Stripe API version bumps, so plan for an upgrade treadmill with a migration read each time
- Your type checking runs in CI and you install loosely: the project explicitly exempts type annotations from semver, so a minor release can turn a green build red until you pin to a minor range like stripe~=15.4
- You copy code from tutorials or LLMs without checking: most material on the internet shows the legacy stripe.PaymentIntent.create style, and mixing that global-state pattern with StripeClient in one codebase is a recurring source of confusion and wrong-key bugs
- You only call two or three endpoints from a constrained environment: this is a very large generated package covering every Stripe product, and a thin httpx wrapper around the REST API can be less to install, audit, and upgrade
- You dislike phone-home defaults: the library sends latency and feature-usage telemetry to Stripe unless you set stripe.enable_telemetry = False
Setup reality
pip install stripe is pure Python, no compiling, Python 3.9 or newer. The friction is version management, not installation. Majors arrive several times a year as Stripe revs its API, so pin a minor range (stripe~=15.4) or type errors and behavior changes walk in through routine dependency updates; preview builds with a and b suffixes (15.5.0a2, 15.5.0b1) sit on PyPI next to stable, so never install with a pre-release-permissive resolver flag. For async you also need an async HTTP library; stripe[async] installs httpx for you, but note the split default: sync requests use requests, async requests use httpx, and Stripe recommends explicitly constructing one client rather than relying on that autodetection. Finally, decide up front whether you write StripeClient or legacy style, because half your team pasting old snippets gives you both.
Patterns
Create a StripeClient (the current-style entry point)client-setup
import os
from stripe import StripeClient
client = StripeClient(
os.environ["STRIPE_SECRET_KEY"],
max_network_retries=2,
)
customers = client.v1.customers.list(params={"limit": 3})This is the v8+ pattern; stripe.api_key plus stripe.Customer.list() is the legacy style most old tutorials show, and once it is deprecated new endpoints will be StripeClient-only. Retries are off unless you set max_network_retries; the library adds idempotency keys to its own retries automatically.
Create a PaymentIntent for a custom payment flowcreate-payment-intent
intent = client.v1.payment_intents.create(
params={
"amount": 2000,
"currency": "usd",
"automatic_payment_methods": {"enabled": True},
},
)
# send intent.client_secret to your frontend to confirm
return {"clientSecret": intent.client_secret}StripeClient services take a single params dict, not keyword arguments; kwargs is the legacy calling convention. amount is in the smallest currency unit, so 2000 means $20.00, and zero-decimal currencies like JPY take the whole amount.
Create a Checkout Session (hosted payment page)checkout-session
session = client.v1.checkout.sessions.create(
params={
"mode": "payment",
"line_items": [{"price": "price_1ABC...", "quantity": 1}],
"success_url": "https://example.com/thanks?session_id={CHECKOUT_SESSION_ID}",
"cancel_url": "https://example.com/cart",
},
)
return redirect(session.url, code=303)The {CHECKOUT_SESSION_ID} placeholder is substituted by Stripe, so keep it literal in the URL. Do not treat the user landing on success_url as proof of payment; confirm via the checkout.session.completed webhook, since users can close the tab or replay the URL.
Verify a webhook signature and parse the eventverify-webhook-signature
import stripe
from flask import request
@app.post("/stripe/webhook")
def webhook():
payload = request.get_data() # raw bytes, not request.json
sig = request.headers.get("Stripe-Signature", "")
try:
event = client.construct_event(payload, sig, WEBHOOK_SECRET)
except stripe.SignatureVerificationError:
return "bad signature", 400
if event.type == "checkout.session.completed":
fulfill(event.data.object)
return "", 200Verification runs over the exact raw body; any middleware that parses and re-serializes JSON changes the bytes and every signature fails. construct_event rejects timestamps older than 300 seconds by default to block replays. WEBHOOK_SECRET is the whsec_... endpoint secret, not your API key.
Iterate every record without manual pagingauto-pagination
for customer in client.v1.customers.list(
params={"limit": 100},
).auto_paging_iter():
process(customer)auto_paging_iter() follows has_more cursors lazily, so limit is the page size per request, not a cap on total results. Wrapping it in list() loads everything into memory, which on a big account is a lot of API calls and RAM.
Make a charge safe to retry with an idempotency keyidempotency-key
intent = client.v1.payment_intents.create(
params={"amount": 2000, "currency": "usd"},
options={"idempotency_key": f"order-{order.id}-charge"},
)The library auto-generates keys only for its own network retries; if your app or queue retries the call, you need your own key derived from the business operation or you will double-charge. Stripe stores keys for 24 hours, and replaying a key with different params returns an error.
Handle the error classes that actually occurerror-handling
import stripe
try:
intent = client.v1.payment_intents.create(params={...})
except stripe.CardError as e:
show_user(e.user_message) # safe to display
except stripe.RateLimitError:
retry_later()
except stripe.InvalidRequestError as e:
log.error("bad params: %s (param=%s)", e, e.param)
raise
except stripe.StripeError as e:
log.error("stripe failure, request_id=%s", e.request_id)
raiseExceptions are importable straight from the stripe module; older code spells them stripe.error.CardError, which still resolves but is the legacy path. Only CardError.user_message is written for end users; other messages can leak internals. Log request_id, it is what Stripe support asks for.
Pin the Stripe API versionpin-api-version
client = StripeClient(
os.environ["STRIPE_SECRET_KEY"],
stripe_version="2019-02-19", # or per request:
)
client.v1.customers.list(
options={"stripe_version": "2019-02-19"},
)The SDK's types describe whichever API version was current at its release, so overriding stripe_version means runtime shapes can diverge from what the type checker believes. Usually you should upgrade the SDK instead of pinning an old API version, and keep webhook endpoint versions in sync too.
Use the async API in an async frameworkasync-usage
import stripe
from stripe import StripeClient
client = StripeClient(
os.environ["STRIPE_SECRET_KEY"],
http_client=stripe.HTTPXClient(),
)
async def get_customer(cid: str):
return await client.v1.customers.retrieve_async(cid)
async def all_customers():
page = await client.v1.customers.list_async(params={"limit": 100})
async for c in page.auto_paging_iter():
yield cEvery request method has an _async twin; there is no separate async client class. Install with pip install stripe[async] (v13.0.1+) to pull httpx. An explicit HTTPXClient raises if you call sync methods, which is a feature: pass allow_sync_methods=True only if you really mix both.
Act on behalf of a connected account (Stripe Connect)connect-account
account_intent = client.v1.payment_intents.create(
params={"amount": 2000, "currency": "usd"},
options={"stripe_account": "acct_1ABC..."},
)options={"stripe_account": ...} sets the Stripe-Account header per request, which is the Connect authentication model. Setting it on the StripeClient constructor scopes every call from that client to the connected account, so keep a platform-level client separate.
Expand nested objects to skip extra API callsexpand-nested-objects
sub = client.v1.subscriptions.retrieve(
"sub_1ABC...",
params={"expand": ["latest_invoice.payment_intent", "customer"]},
)
status = sub.latest_invoice.payment_intent.statusWithout expand these fields are bare ID strings and touching .status is an AttributeError. Expansion goes up to four levels deep with dot paths, but each expansion makes the response bigger and slower, so expand only what you read.
Call a beta or undocumented endpointraw-request
response = client.raw_request(
"post",
"/v1/beta_endpoint",
param=123,
stripe_version="2022-11-15; feature_beta=v3",
)
obj = client.deserialize(response, api_mode="V1")Available since v11, this bypasses the generated method definitions, which is how you reach private-beta endpoints or send parameters the SDK does not know yet. You lose all typing; deserialize gives back a dynamic StripeObject, not a typed resource.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| dj-stripe | PyPI | Django projects that want Stripe objects mirrored into database models with webhook-driven sync instead of raw SDK calls |
| braintree | PyPI | You are on Braintree/PayPal as your processor rather than Stripe; the choice of SDK follows the processor, not taste |
| adyen | PyPI | Enterprise setups on Adyen, common when a business already routes cards through it in physical retail |
| httpx | PyPI | You call a handful of Stripe endpoints and would rather own a 100-line client than ride the SDK's major-version treadmill |