2174 words
11 minutes
Integrating Stripe Payments in Python Web Apps Using Flask

Integrating Stripe Payments in Python Web Applications Using Flask#

Securely handling online payments is a critical component for many web applications. Stripe provides a robust and developer-friendly platform for processing transactions. Integrating Stripe into a Python web application using the Flask microframework offers a flexible and efficient way to build payment functionality. This integration typically involves server-side logic in Flask to interact with the Stripe API and client-side code to handle user input and the payment flow.

Understanding Essential Concepts#

Successful Stripe integration requires familiarity with key components and workflows:

  • Payment Gateway: A service, like Stripe, that authorizes and processes online payments. It acts as an intermediary between the merchant’s website, the customer’s bank, and the merchant’s bank.
  • API (Application Programming Interface): A set of rules and protocols that allows different software applications to communicate with each other. The Stripe API enables developers to programmatically create charges, manage customers, handle subscriptions, and access other Stripe features from their application’s backend.
  • Stripe API Keys: Credentials used to authenticate requests to the Stripe API.
    • Publishable Key: Used on the client-side (frontend) to securely identify the account and create tokens without exposing sensitive information. It typically starts with pk_test_ or pk_live_.
    • Secret Key: Used on the server-side (backend) to make authenticated API calls for creating charges, refunds, etc. It must be kept confidential and never exposed in client-side code. It typically starts with sk_test_ or sk_live_.
  • Payment Intents: The recommended API object for handling the lifecycle of a payment. It tracks the payment process, requiring confirmation when additional steps (like 3D Secure authentication) are needed. This modern approach improves handling complex payment flows compared to older methods like direct Charges.
  • Stripe Elements: Customizable UI components for building payment forms. They are pre-built, secure, and handle sensitive card details, ensuring PCI compliance for the merchant. Elements securely send payment information directly to Stripe, providing a token or payment method ID to the server without sensitive data touching the application’s backend.
  • Webhooks: Automated messages sent by Stripe to a specified endpoint in the application’s backend when certain events occur (e.g., a payment succeeds, a subscription renews, a refund is issued). Webhooks are crucial for handling asynchronous events and updating the application’s state based on actions happening within Stripe’s system.

Why Integrate Stripe with Flask?#

Combining Stripe’s payment processing capabilities with Flask’s simplicity and flexibility provides several advantages:

  • Ease of Development: Flask is known for its minimalist design, allowing developers to build web applications quickly. Stripe’s comprehensive API documentation and Python library integrate seamlessly with Flask routes and logic.
  • Security: Stripe handles the complexity of PCI compliance by providing secure methods for collecting payment information (like Stripe Elements) and processing transactions, reducing the burden on the application developer. Server-side integration using Flask ensures sensitive API keys remain protected.
  • Feature Richness: Stripe offers a wide array of features beyond simple payments, including subscriptions, invoicing, fraud prevention (Radar), and reporting, all accessible via its API and manageable within a Flask application.
  • Scalability: Both Flask and Stripe are designed to handle varying levels of traffic and transaction volume, making the combination suitable for applications ranging from small projects to large-scale services.

Prerequisites for Integration#

Before beginning the integration process, certain prerequisites must be in place:

  • A working Python environment.
  • Flask installed (pip install Flask).
  • The official Stripe Python library installed (pip install stripe).
  • A Stripe account (test mode is sufficient for development).
  • Stripe API keys (publishable and secret keys) obtained from the Stripe Dashboard.
  • (Optional but Recommended for webhooks) A tool like ngrok to expose the local development server to the internet for receiving webhook events during testing (pip install ngrok) or a deployed application endpoint.

Step-by-Step Flask and Stripe Integration (Payment Intents)#

This section outlines the process for integrating Stripe to handle a one-time payment using the Payment Intents API and Stripe Elements.

1. Setting up the Flask Project#

Create a basic Flask application structure.

my_app/
├── app.py
└── templates/
└── checkout.html

app.py:

from flask import Flask, render_template, request, jsonify
import stripe
import os
from dotenv import load_dotenv
# Load environment variables from a .env file
load_dotenv()
app = Flask(__name__)
# Load Stripe API keys from environment variables
stripe.api_key = os.getenv('STRIPE_SECRET_KEY')
stripe_publishable_key = os.getenv('STRIPE_PUBLISHABLE_KEY')
@app.route('/')
def index():
return render_template('index.html') # Or redirect to checkout
@app.route('/checkout')
def checkout():
return render_template('checkout.html', stripe_publishable_key=stripe_publishable_key)
# Add other routes later
if __name__ == '__main__':
app.run(debug=True)

Create a .env file in the project root:

STRIPE_SECRET_KEY=sk_test_YOUR_SECRET_KEY
STRIPE_PUBLISHABLE_KEY=pk_test_YOUR_PUBLISHABLE_KEY

Replace YOUR_SECRET_KEY and YOUR_PUBLISHABLE_KEY with keys from your Stripe dashboard. Never commit your .env file to version control.

templates/index.html (simple link to checkout):

<!DOCTYPE html>
<html>
<head>
<title>Welcome</title>
</head>
<body>
<h1>Welcome!</h1>
<p><a href="/checkout">Go to Checkout</a></p>
</body>
</html>

2. Creating the Checkout Page (HTML with Stripe Elements)#

The frontend uses Stripe Elements to securely collect payment details.

templates/checkout.html:

<!DOCTYPE html>
<html>
<head>
<title>Checkout</title>
<script src="https://js.stripe.com/v3/"></script>
</head>
<body>
<h1>Complete Your Purchase</h1>
<form id="payment-form">
<div id="card-element">
<!-- Stripe Elements will create input elements here -->
</div>
<button id="submit">Pay</button>
<div id="payment-message" role="alert">
<!-- Display error or success messages here -->
</div>
</form>
<script>
const stripe = Stripe("{{ stripe_publishable_key }}");
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
const form = document.getElementById('payment-form');
const paymentMessage = document.getElementById('payment-message');
form.addEventListener('submit', async function(event) {
event.preventDefault();
// Disable button while processing
document.getElementById('submit').disabled = true;
// 1. Create a Payment Intent on the server
const response = await fetch('/create-payment-intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ amount: 1000 }), // Example amount in cents (e.g., $10.00)
});
const { clientSecret } = await response.json();
if (response.ok) {
// 2. Confirm the card payment using the client secret
const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: cardElement,
}
});
if (error) {
// Show error to the user
paymentMessage.textContent = error.message;
document.getElementById('submit').disabled = false; // Re-enable button
} else {
// Show a success message or redirect
if (paymentIntent.status === 'succeeded') {
paymentMessage.textContent = 'Payment Succeeded!';
// You would typically redirect the user to a success page
// window.location.href = '/success';
} else {
paymentMessage.textContent = 'Payment status: ' + paymentIntent.status;
}
}
} else {
// Handle server-side error creating Payment Intent
const errorData = await response.json();
paymentMessage.textContent = 'Error creating Payment Intent: ' + (errorData.error || 'Unknown error');
document.getElementById('submit').disabled = false; // Re-enable button
}
});
</script>
</body>
</html>
  • The Stripe.js library (<script src="https://js.stripe.com/v3/"></script>) is loaded.
  • An instance of Stripe is initialized with the publishable key.
  • stripe.elements() creates an elements instance, which is used to create individual UI components like the card element.
  • cardElement.mount('#card-element') attaches the card input field to the specified HTML element.
  • When the form is submitted, the JavaScript:
    • Prevents the default form submission.
    • Makes an asynchronous request (fetch) to a Flask backend route (/create-payment-intent) to create a Payment Intent.
    • Receives the clientSecret from the backend.
    • Uses stripe.confirmCardPayment with the clientSecret and the cardElement to finalize the payment securely on the client-side via Stripe.js.
    • Displays messages based on the result (success or error).

3. Handling the Payment Request (Flask Backend)#

Create a Flask route to handle the request from the frontend to create a Payment Intent.

Add this route to your app.py:

@app.route('/create-payment-intent', methods=['POST'])
def create_payment():
try:
data = request.get_json()
amount = data['amount'] # Amount in cents
# Create a PaymentIntent with the order amount and currency
intent = stripe.PaymentIntent.create(
amount=amount,
currency='usd', # Or your currency
automatic_payment_methods={
'enabled': True,
},
# Add metadata if needed (e.g., order_id)
metadata={'integration_parameter': 'flask'}
)
return jsonify({
'clientSecret': intent.client_secret
})
except Exception as e:
return jsonify(error=str(e)), 403 # Use 403 or appropriate error code
  • This route listens for POST requests on /create-payment-intent.
  • It retrieves the payment amount from the incoming JSON data (sent by the frontend).
  • stripe.PaymentIntent.create() calls the Stripe API using your secret key to create a new Payment Intent object. The amount and currency are specified. automatic_payment_methods enables Stripe to handle multiple payment methods automatically.
  • The client_secret from the created Payment Intent is returned to the frontend as JSON. This client_secret is essential for the frontend to confirm the payment.

4. Handling Asynchronous Events with Webhooks#

While the Payment Intent status (succeeded) can be checked on the frontend after confirmCardPayment, relying solely on this can be unreliable (e.g., if the user closes the browser quickly). Webhooks provide a robust way to confirm payment success and trigger backend actions (like fulfilling an order, sending a confirmation email) reliably.

Setting up a Webhook Endpoint#

Create a Flask route to receive POST requests from Stripe’s webhook service.

Add this route to app.py:

# Use environment variable for webhook secret
webhook_secret = os.getenv('STRIPE_WEBHOOK_SECRET')
@app.route('/webhook', methods=['POST'])
def webhook():
payload = request.data
sig_header = request.headers.get('Stripe-Signature')
event = None
try:
# Verify webhook signature
event = stripe.Webhook.construct_event(
payload, sig_header, webhook_secret
)
except ValueError as e:
# Invalid payload
return 'Invalid payload', 400
except stripe.error.SignatureVerificationError as e:
# Invalid signature
return 'Invalid signature', 400
# Handle the event
if event['type'] == 'payment_intent.succeeded':
payment_intent = event['data']['object'] # contains a stripe.PaymentIntent
print('PaymentIntent was successful!')
# TODO: Implement logic to fulfill the order
# Example: Check payment_intent.metadata for order ID, update database, send email, etc.
print(f"PaymentIntent ID: {payment_intent['id']}, Amount: {payment_intent['amount']}, Currency: {payment_intent['currency']}")
# Access metadata: print(payment_intent.get('metadata', {}).get('order_id'))
elif event['type'] == 'payment_method.attached':
payment_method = event['data']['object'] # contains a stripe.PaymentMethod
print('PaymentMethod was attached to a Customer!')
# Can be used for saving cards for future payments
# ... handle other event types
# See https://stripe.com/docs/api/events/types
return 'Success', 200

Add the webhook secret to your .env file:

# ... other keys
STRIPE_WEBHOOK_SECRET=whsec_YOUR_WEBHOOK_SECRET
  • Stripe sends webhook events as POST requests to a configured URL.
  • The application receives the raw payload and the Stripe-Signature header.
  • stripe.Webhook.construct_event() is used to verify the signature against the STRIPE_WEBHOOK_SECRET. This is critical for security to ensure the event came from Stripe and hasn’t been tampered with.
  • Based on the event['type'], the appropriate logic is executed. For a one-time payment using Payment Intents, payment_intent.succeeded is the most important event for order fulfillment.
  • The function must return a 200 OK response quickly to acknowledge receipt of the event.

Setting up the Webhook Endpoint in Stripe#

  1. Go to the Stripe Dashboard.
  2. Navigate to Developers -> Webhooks.
  3. Click Add endpoint.
  4. Enter the URL where Stripe should send events. During local development, use a tool like ngrok.
    • Start ngrok: ngrok http 5000 (if your Flask app runs on port 5000). Ngrok provides a public URL (e.g., https://abcdef123456.ngrok.io). Use https://abcdef123456.ngrok.io/webhook.
    • For production, this would be the public URL of your deployed application’s /webhook endpoint.
  5. Select the events you want to listen for (e.g., payment_intent.succeeded).
  6. Click Add endpoint.
  7. After creation, Stripe provides a Webhook secret for that endpoint. Copy this secret and add it to your .env file as STRIPE_WEBHOOK_SECRET.

Real-World Considerations and Examples#

  • Order Fulfillment: The primary logic for completing a purchase (e.g., updating inventory, creating a database record for the order, sending confirmation emails) should happen within the payment_intent.succeeded webhook handler. This ensures fulfillment occurs only after Stripe confirms the payment has cleared.
  • Idempotency: Webhooks can occasionally be delivered more than once. Implement idempotency in your webhook handler to ensure that processing an event multiple times does not cause issues (e.g., accidentally fulfilling an order twice). Stripe provides an idempotency_key in the request header for this purpose, or you can track processed events in your database.
  • Error Handling: Implement robust error handling on both the frontend (displaying user-friendly messages for card errors) and the backend (logging API errors, handling invalid requests).
  • Customer Management: For recurring payments or saved cards, create Stripe Customer objects and associate Payment Methods with them.
  • Metadata: Attach relevant metadata (like your internal order ID, customer ID) to Stripe objects (Payment Intents, Charges, Customers). This helps connect Stripe transactions back to your application’s data and is visible in the Stripe Dashboard. metadata={'order_id': 'ORD12345'}
# Example metadata added to PaymentIntent creation
intent = stripe.PaymentIntent.create(
amount=amount,
currency='usd',
automatic_payment_methods={'enabled': True},
metadata={'order_id': 'YOUR_INTERNAL_ORDER_ID', 'user_id': 'USER_ID'}
)
# Accessing metadata in webhook
if event['type'] == 'payment_intent.succeeded':
payment_intent = event['data']['object']
order_id = payment_intent.get('metadata', {}).get('order_id')
user_id = payment_intent.get('metadata', {}).get('user_id')
print(f"Payment for order {order_id} by user {user_id} succeeded.")
# Use order_id to find and fulfill the order in your database
  • Amount Representation: Stripe API amounts are specified in the smallest currency unit (e.g., cents for USD, yen for JPY). Ensure your application logic correctly converts currency amounts (e.g., multiplying dollar amounts by 100).
  • HTTPS: Your production webhook endpoint must use HTTPS. Sensitive information could be intercepted otherwise.

Security Best Practices#

  • Protect Secret Keys: Never expose your Stripe secret key on the client-side or embed it directly in your code. Use environment variables and load them securely on the server.
  • Validate Webhook Signatures: Always verify the Stripe-Signature header in webhook requests using stripe.Webhook.construct_event. This prevents attackers from sending fake webhook events to your application.
  • Use HTTPS: Ensure your entire application, especially pages handling payment information and webhook endpoints, is served over HTTPS.
  • PCI Compliance: By using Stripe Elements and Stripe.js to handle card details directly in the customer’s browser, sensitive card data never touches your server, significantly reducing your PCI compliance scope.
  • Logging and Monitoring: Implement logging for API requests, responses, and webhook events to aid in debugging and monitoring.

Key Takeaways#

  • Integrating Stripe with Flask involves both server-side (Flask) and client-side (HTML, JavaScript) development.
  • The Stripe Python library simplifies server-side interactions with the Stripe API.
  • The Payment Intents API is the modern approach for handling one-time payments, managing the payment lifecycle.
  • Stripe Elements provides secure, pre-built UI components for collecting payment details on the frontend, aiding PCI compliance.
  • The client_secret acts as a bridge between the server-created Payment Intent and the client-side confirmation.
  • Webhooks are essential for reliably handling asynchronous payment events (like payment_intent.succeeded) and triggering backend order fulfillment logic.
  • Securely handle API keys using environment variables and validate webhook requests using the provided signature.
  • Always use HTTPS for production environments.
  • Stripe API amounts are in the smallest currency unit (e.g., cents).
Integrating Stripe Payments in Python Web Apps Using Flask
https://dev-resources.site/posts/integrating-stripe-payments-in-python-web-apps-using-flask/
Author
Dev-Resources
Published at
2025-06-29
License
CC BY-NC-SA 4.0