Skip to content

Payment Intents — Custom Payment Flows and Confirmation

DodaTech Updated 2026-06-28 4 min read

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

Stripe Payment Intents track the lifecycle of a payment from creation to completion, handling authentication, authorization, and confirmation with support for 3D Secure and multiple payment methods.

What You'll Learn

By the end of this lesson you will understand how to create Payment Intents, handle payment confirmation, manage 3D Secure authentication, and build custom payment flows for your frontend.

Why It Matters

Checkout is great for quick integration, but Payment Intents give you full control over the payment flow. You can build custom forms, handle complex authentication scenarios, and integrate with your existing UI.

Real-World Use

DodaBrowser's premium features use Payment Intents with Stripe Elements for a custom payment form that matches the application's design while keeping card data on Stripe's servers.

flowchart LR
    F[Frontend] --> B[Backend: Create PaymentIntent]
    B --> S[Stripe]
    S -->|client_secret| B
    B -->|client_secret| F
    F -->|Confirm with card| S
    S -->|3DS if needed| F
    F -->|Post-payment| B
    B -->|Webhook confirm| S
    style S fill:#6772e5,color:#fff

Creating a Payment Intent

The Payment Intent is created server-side with the amount, currency, and payment method. The client secret is returned to the frontend for confirmation.

import stripe
import os

stripe.api_key = os.environ.get("STRIPE_SECRET_KEY", "sk_test_placeholder")

def create_payment_intent(amount_cents, currency="usd"):
    intent = stripe.PaymentIntent.create(
        amount=amount_cents,
        currency=currency,
        automatic_payment_methods={"enabled": True},
    )
    return intent.client_secret

stripe.api_key = "sk_test_placeholder"

class MockPI:
    client_secret = "pi_abc123_secret_xyz789"

stripe.PaymentIntent.create = lambda **kwargs: MockPI()

client_secret = create_payment_intent(2999)
print(f"Client Secret: {client_secret}")
print("Send this to frontend for confirmation")

Expected output:

Client Secret: pi_abc123_secret_xyz789
Send this to frontend for confirmation

Confirmation and 3D Secure

The frontend confirms the payment using the client secret. If 3D Secure (Strong Customer Authentication) is required, Stripe handles the authentication flow.

# confirmation_flow.py
# Payment Intent confirmation flow

def simulate_confirmation(intent_id, payment_method_id):
    print(f"Confirming PaymentIntent {intent_id}")
    print(f"Payment Method: {payment_method_id}")
    
    statuses = ["requires_confirmation", "processing", "requires_action", "succeeded"]
    
    print("\nPayment Intent Lifecycle:")
    print("  Created -> RequiresConfirmation -> Processing -> RequiresAction(3DS) -> Succeeded")
    
    if payment_method_id.startswith("pm_card_authenticationRequired"):
        print("\n  >>> 3D Secure required!")
        print("  >>> Redirect customer for authentication")
        print("  >>> After authentication, confirm again")
        return {"status": "requires_action", "next_action": {"type": "use_stripe_sdk"}}
    
    print("\n  >>> Payment confirmed successfully!")
    return {"status": "succeeded", "payment_intent": intent_id}

result1 = simulate_confirmation("pi_1", "pm_card_visa")
print(f"Status: {result1['status']}\n")

result2 = simulate_confirmation("pi_2", "pm_card_authenticationRequired")
print(f"Status: {result2['status']}")

Handling Payment Method Types

Payment Intents support many payment methods. Configure which ones to show based on customer location.

# payment_methods.py
# Configuring payment methods

def configure_payment_methods(country):
    methods_map = {
        "US": ["card", "us_bank_account"],
        "DE": ["card", "ideal", "sepa_debit"],
        "NL": ["ideal", "card"],
        "GB": ["card", "bacs_debit"],
        "default": ["card"]
    }
    
    methods = methods_map.get(country, methods_map["default"])
    print(f"Payment methods for {country}: {', '.join(methods)}")
    return methods

countries = ["US", "DE", "NL", "FR", "JP"]
for c in countries:
    configure_payment_methods(c)

Common Mistakes

  1. Not handling requires_action status: Payment Intents may require 3D Secure. Your frontend must handle the requires_action status.

  2. Confirming from the server without client_secret: Use the client secret on the frontend with Stripe.js, not from the server.

  3. Forgetting to capture vs separate authorization and capture: By default Payment Intents capture immediately. Use capture_method: manual for delayed capture.

  4. Not handling Webhooks for async payment methods: Bank transfers and other async methods complete outside the browser. Listen for payment_intent.succeeded Webhook.

  5. Storing raw card data: Never store full card numbers. Use PaymentMethod IDs from Stripe Elements or Checkout.

Practice Questions

  1. What is a Payment Intent? An object representing the intent to collect a payment, tracking the payment from creation through confirmation and completion.

  2. What is the client secret? A secret key unique to each Payment Intent used by the frontend to securely confirm the payment.

  3. How does 3D Secure work with Payment Intents? If the card requires authentication, the Payment Intent enters requires_action status and the frontend uses Stripe.js to handle the authentication.

  4. What is the difference between automatic and manual capture? Automatic captures immediately. Manual capture holds the amount for up to 7 days before capturing.

  5. Challenge: Build a complete Payment Intent flow with server-side creation, frontend confirmation using Stripe Elements, and webhook confirmation.

FAQ

Can I update a Payment Intent after creation?

Yes. You can update the amount, currency, description, and other parameters before confirmation.

What happens if a Payment Intent fails?

The status changes to requires_payment_method. You can retry with a different payment method.

How long is a Payment Intent valid?

Payment Intents expire after 1 hour. Create them close to the time of confirmation.

Can I cancel a Payment Intent?

Yes. Set the status to canceled. Canceled intents cannot be confirmed.

What is the maximum Payment Intent amount?

The maximum amount depends on the currency. For USD, the maximum is $999,999.99.

Mini Project

Create a Payment Intent Serverless endpoint that creates a Payment Intent, returns the client secret, and handles the webhook confirmation.

import json

def lambda_handler(event, context):
    body = json.loads(event.get("body", "{}"))
    amount = body.get("amount", 1999)
    currency = body.get("currency", "usd")
    
    intent_id = f"pi_{hash(str(amount))}_test"
    client_secret = f"{intent_id}_secret_test"
    
    print(f"Created PaymentIntent: {intent_id}")
    print(f"Amount: ${amount/100:.2f} {currency.upper()}")
    
    return {
        "statusCode": 200,
        "body": json.dumps({
            "clientSecret": client_secret,
            "intentId": intent_id,
            "amount": amount,
            "currency": currency
        })
    }

result = json.loads(lambda_handler({"body": json.dumps({"amount": 4999})}, None)["body"])
print(f"Client Secret: {result['clientSecret']}")

What's Next

Next: Subscription Creation for recurring billing.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro