mrkeyoor.com_
Thu 06 Aug 23:58 UTC
PyPIWeb Backendupdated 06 Aug 2026

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.

Verdict

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.

API stability3/5Two API styles coexist: StripeClient arrived in v8 and the legacy module pattern is still working but marked for future deprecation. Majors track Stripe API versions (v8 to v15 in about two years) and type annotations are explicitly exempt from semver, so minors can break type checks.
Docs4/5The Stripe API reference has Python samples for every endpoint and is among the best in the industry, plus migration guides per major on the GitHub wiki. Docked one point because reference samples still mix legacy and StripeClient styles, and the README defers most substance off-repo.
Maintenance5/5Corporate-backed and very active: pushed August 6, 2026, v15.4.0 released July 29, 2026, with parallel alpha and beta preview channels and only 24 open issues counting PRs. Caveat: first-time external contributions are on hiatus, so outsiders can file issues but not PRs.
Ecosystem5/5About 10.1M weekly downloads and it is the default payments dependency in Python web development; dj-stripe, SaaS boilerplates, and virtually every Stripe tutorial build on it. Being official, it gets every new Stripe product on day one.

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
Skip it if

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 "", 200

Verification 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)
    raise

Exceptions 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 c

Every 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.status

Without 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

PackageRegistryPick it when
dj-stripePyPIDjango projects that want Stripe objects mirrored into database models with webhook-driven sync instead of raw SDK calls
braintreePyPIYou are on Braintree/PayPal as your processor rather than Stripe; the choice of SDK follows the processor, not taste
adyenPyPIEnterprise setups on Adyen, common when a business already routes cards through it in physical retail
httpxPyPIYou call a handful of Stripe endpoints and would rather own a 100-line client than ride the SDK's major-version treadmill