Payment Intents — Custom Payment Flows and Confirmation
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
Not handling requires_action status: Payment Intents may require 3D Secure. Your frontend must handle the requires_action status.
Confirming from the server without client_secret: Use the client secret on the frontend with Stripe.js, not from the server.
Forgetting to capture vs separate authorization and capture: By default Payment Intents capture immediately. Use capture_method: manual for delayed capture.
Not handling Webhooks for async payment methods: Bank transfers and other async methods complete outside the browser. Listen for payment_intent.succeeded Webhook.
Storing raw card data: Never store full card numbers. Use PaymentMethod IDs from Stripe Elements or Checkout.
Practice Questions
What is a Payment Intent? An object representing the intent to collect a payment, tracking the payment from creation through confirmation and completion.
What is the client secret? A secret key unique to each Payment Intent used by the frontend to securely confirm the payment.
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.
What is the difference between automatic and manual capture? Automatic captures immediately. Manual capture holds the amount for up to 7 days before capturing.
Challenge: Build a complete Payment Intent flow with server-side creation, frontend confirmation using Stripe Elements, and webhook confirmation.
FAQ
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