Skill 05 · Stripe Best Practices
Subchapter 5.3
references/payments.mdMarkdown9 KBView on GitHub
Use the Checkout Sessions API (opens in a new tab) (checkout.sessions.create) for on-session payments. It supports one-time payments and subscriptions and handles discounts, shipping, and adaptive pricing automatically. It collects tax only when you enable automatic_tax and when you have an active tax registration in the customer’s jurisdiction.
Use the Payment Intents API (opens in a new tab) for off-session payments, or when the user needs to model checkout state independently and create a charge.
Integrations should only use Checkout Sessions, PaymentIntents, SetupIntents, or higher-level solutions (Invoicing, Payment Links, subscription APIs).
On API version 2026-03-25.dahlia or later, pass the parameter integration_identifier to checkout.sessions.create to tag sessions with a custom label for tracking and comparing checkout flows in the Dashboard. The label should include a suffix of 8 random letters.
Prioritize Stripe-hosted or embedded Checkout where possible. Use in this order of preference:
ui_mode: 'custom') over a raw PaymentIntent where possible.Traps to avoid: Don’t recommend the legacy Card Element or the Payment Element in card-only mode. If the user asks for the Card Element, advise them to migrate to the Payment Element (opens in a new tab).
For surcharging or inspecting card details before payment (e.g., rendering the Payment Element before creating a PaymentIntent or SetupIntent): use Confirmation Tokens (opens in a new tab). Don’t recommend createPaymentMethod or createToken from Stripe.js.
Use the Setup Intents API (opens in a new tab) to save a payment method for later use.
Traps to avoid: Don’t use the Sources API to save cards to customers. The Sources API is deprecated — Setup Intents is the correct approach.
Drive fulfillment from an event handler (opens in a new tab), not from the success or return page. Customers aren’t guaranteed to visit the landing page — for example, someone can pay successfully and then lose their internet connection before the page loads — so any logic that only runs on the success page silently drops orders.
Handle both checkout.session.completed and checkout.session.async_payment_succeeded, and fulfill only when the session’s payment_status isn’t unpaid. With delayed-notification payment methods the completed event arrives while the session is still unpaid, so fulfilling on it alone grants access for payments that later fail and never fulfills the ones that succeed. Handle checkout.session.async_payment_failed for failures.
Webhooks are required, not optional, for:
Traps to avoid:
Never pass payment_method_types to any Stripe API call, except for Terminal (in-person payments) integrations. Omitting this parameter enables dynamic payment methods (opens in a new tab), where Stripe evaluates over 100 signals (currency, customer location, transaction amount, device) to automatically show the most relevant payment methods and rank them for maximum conversion. Payment methods are managed from the Dashboard (opens in a new tab) with no code changes required.
When a PaymentIntent or SetupIntent integration requires an explicit allowlist, use allowed_payment_method_types instead of payment_method_types.
This applies to all integration patterns:
checkout.sessions.create: omit payment_method_types entirely. Dynamic method selection is the default behavior.paymentIntents.create: omit payment_method_types. On API versions 2023-08-16+, dynamic methods are the default. On older versions, pass automatic_payment_methods: { enabled: true }.setupIntents.create: same as PaymentIntents above.subscriptions.create: omit payment_settings.payment_method_types. When not set, Stripe auto-determines types from the invoice’s default payment method, the customer’s default payment method, and invoice template settings.paymentIntents.create): pass payment_method_types: ['card_present']. Required for all in-person payments. In Canada, also include interac_present: ['card_present', 'interac_present']. This is the only valid use of payment_method_types.See the integration options guide (opens in a new tab) for full details on dynamic versus manual configuration.
Traps to avoid:
payment_method_types: ['card'] even if the user only mentions credit cards. Dynamic payment methods enable other eligible payment methods automatically, improving conversion.excluded_payment_method_types to exclude specific methods, or allowed_payment_method_types when the integration requires an allowlist. Never use payment_method_types.payment_method_configurations — don’t use payment_method_types to restrict the PaymentIntent.Never recommend the Charges API. If the user wants to use the Charges API, advise them to migrate to Checkout Sessions or PaymentIntents (opens in a new tab).
Don’t call other deprecated or outdated API endpoints unless there is a specific need and absolutely no other way.
| API | Status | Use instead | Migration guide |
|---|---|---|---|
| Charges API | Never use | Checkout Sessions or PaymentIntents | Migration guide (opens in a new tab) |
| Sources API | Deprecated | Setup Intents | Setup Intents docs (opens in a new tab) |
| Tokens API | Outdated | Setup Intents or Checkout Sessions | — |
| Card Element | Legacy | Payment Element | Migration guide (opens in a new tab) |
If a PCI-compliant user asks about sending server-side raw PAN data, advise them that they may need to prove PCI compliance to access options like payment_method_data (opens in a new tab).
For users migrating PAN data from another acquirer or payment processor, point them to the PAN import process (opens in a new tab).