Skip to content

Stripe Payment Intents: Modern One-Time Payment Processing

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Stripe Payment Intents: Modern One. We cover key concepts, practical examples, and best practices to help you master this topic.

Payment Intents track a payment from creation through authentication and capture, supporting Strong Customer Authentication (3D Secure), dynamic payment methods, and status management.

What You'll Learn

How to create and confirm PaymentIntents, handle 3D Secure authentication, manage payment statuses, save payment methods for reuse, and confirm with saved methods.

Why It Matters

Payment Intents are Stripe's modern payment API, required for SCA Compliance and supporting 135+ payment methods. DodaTech uses PaymentIntents for all one-time purchases with automatic 3D Secure handling.

Real-World Use

A customer buys a Pro plan. The server creates a PaymentIntent, the client confirms it with card details via Stripe Elements, 3D Secure triggers if needed, and the Webhook confirms success.

flowchart LR
    A["Create\nPaymentIntent"] --> B["Requires\nPaymentMethod"]
    B --> C["Client Confirms\n+ 3D Secure"]
    C --> D["Processing"]
    D --> E["Succeeded"]
    D --> F["Requires\nAction"]
    F --> G["Customer\nAuthenticates"]
    G --> D
    style A fill:#6772e5,color:#fff
    style C fill:#dbeafe,stroke:#2563eb
    style E fill:#bbf7d0,stroke:#16a34a

Creating a PaymentIntent

import stripe
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]

def create_payment_intent(amount, currency="usd"):
    intent = stripe.PaymentIntent.create(
        amount=amount,
        currency=currency,
        automatic_payment_methods={"enabled": True},
        description="Pro Plan Purchase",
        metadata={"order_id": "ORD-12345"}
    )

    print(f"PaymentIntent: {intent.id}")
    print(f"Amount: ${intent.amount/100:.2f}")
    print(f"Status: {intent.status}")
    print(f"Client Secret: {intent.client_secret[:20]}...")

    return intent

intent = create_payment_intent(2999)
# Expected output:
# PaymentIntent: pi_3Mqwerty19QfG5XG0gTzFz1L
# Amount: $29.99
# Status: requires_payment_method

Handling 3D Secure

# Client confirms the PaymentIntent (JavaScript)
# import { confirmCardPayment } from "@stripe/stripe-js";
#
# const { error, paymentIntent } = await stripe.confirmCardPayment(
#   clientSecret,
#   { payment_method: { card: cardElement } }
# );
#
# if (error) {
#   console.log("Payment failed:", error.message);
# } else if (paymentIntent.status === "requires_action") {
#   // 3D Secure - cardholder must authenticate
#   stripe.confirmCardPayment(clientSecret);
# } else if (paymentIntent.status === "succeeded") {
#   console.log("Payment succeeded!");
# }

# Server-side check after confirmation
def check_payment_status(payment_intent_id):
    intent = stripe.PaymentIntent.retrieve(payment_intent_id)
    print(f"Status: {intent.status}")

    if intent.status == "succeeded":
        print("Payment completed successfully")
        return True
    elif intent.status == "requires_action":
        print("3D Secure authentication required — client must complete")
        return False
    elif intent.status == "requires_payment_method":
        print("Payment failed — retry with different method")
        return False

    return False

Saving Payment Methods

def create_payment_intent_with_setup(amount, customer_id):
    intent = stripe.PaymentIntent.create(
        amount=amount,
        currency="usd",
        customer=customer_id,
        setup_future_usage="off_session",
        automatic_payment_methods={"enabled": True}
    )
    print(f"PaymentIntent with save: {intent.id}")
    return intent

# Later, charge off-session:
def charge_saved_method(customer_id, payment_method_id, amount):
    intent = stripe.PaymentIntent.create(
        amount=amount,
        currency="usd",
        customer=customer_id,
        payment_method=payment_method_id,
        off_session=True,
        confirm=True
    )
    print(f"Off-session charge: {intent.id} ({intent.status})")
    return intent

Common Mistakes

1. Not Handling requires_action Status

Many cards require 3D Secure. If you don't handle requires_action, the payment stays pending forever. Always check status and redirect for authentication.

2. Confirming on the Server

Payment Intents should be confirmed on the client side to handle SCA. Server-side confirmation without SCA handling fails for regulated cards.

3. Not Using Idempotency Keys

Network retries without idempotency create duplicate PaymentIntents. Use idempotency_key on all creation requests.

4. Forgetting automatic_payment_methods

Without automatic_payment_methods: { enabled: true }, only card payments work. Enable it to accept wallets, bank transfers, and local methods.

5. Ignoring PaymentIntent Status Transitions

A PaymentIntent can go through requires_payment_method -> requires_action -> processing -> succeeded. Handle each status appropriately.

Practice Questions

  1. What is the difference between PaymentIntent and Charge?
  2. How does 3D Secure work with Payment Intents?
  3. What does setup_future_usage do?
  4. How do you handle failed Payment Intents?

Answers:

  1. PaymentIntent supports SCA, dynamic payment methods, status tracking, and saved payment methods. Charges are deprecated with limited features.
  2. PaymentIntent detects when SCA is needed and returns requires_action status with next_action.redirect_to_url for authentication.
  3. setup_future_usage indicates the payment method should be saved for future off-session charges. Stripe saves it for reuse.
  4. Check intent.status === 'requires_payment_method', get intent.last_payment_error for the reason, let the client retry with a different method.

Challenge: Build a complete PaymentIntent flow: create PI on server with amount + metadata, confirm on client with Stripe Elements, handle 3D Secure if needed, verify success via webhook, and save the payment method for future charges.

FAQ

What payment methods does PaymentIntent support?

Cards, wallets (Apple Pay, Google Pay), bank debits, buy now/pay later (Afterpay, Klarna), bank transfers, and 40+ local payment methods.

How long is a PaymentIntent valid?

PaymentIntents expire after 1 hour if not confirmed. For delayed payment methods, the timeline varies.

Can I change the amount after creation?

No, create a new PaymentIntent for different amounts. You can update metadata before confirmation.

What is the difference between `confirm: true` and manual confirmation?

confirm: true confirms immediately server-side (no SCA). Manual confirmation lets the client handle SCA.

How do I handle card declines?

Stripe returns last_payment_error.decline_code (insufficient_funds, stolen_card, etc.). Show user-friendly messages and suggest alternative methods.

Mini Project

Build a one-time payment flow: server creates PaymentIntent with metadata, client confirms with card input, handle 3D Secure redirect, save payment method for future charges, verify success via webhook, and display confirmation page.

What's Next

Checkout Session — build hosted payment pages with Stripe Checkout.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro