Implementing Stripe Payments in Django: A Complete Guide
# Implementing Stripe Payments in Django
Stripe is the gold standard for payment processing. Here's how we implemented it in DjangoZen for a seamless checkout experience.
## Why Stripe?
- **Developer-friendly API**: Clean, well-documented, and intuitive
- **Global support**: 135+ currencies, 40+ countries
- **Built-in fraud detection**: Radar protects against fraud
- **Webhooks**: Real-time payment status updates
- **PCI compliant**: Handles sensitive card data securely
## Initial Setup
Install the Stripe Python library:
```bash
pip install stripe
```
Configure in settings:
```python
# settings.py
STRIPE_PUBLIC_KEY = os.environ.get('STRIPE_PUBLIC_KEY')
STRIPE_SECRET_KEY = os.environ.get('STRIPE_SECRET_KEY')
STRIPE_WEBHOOK_SECRET = os.environ.get('STRIPE_WEBHOOK_SECRET')
# Initialize Stripe
import stripe
stripe.api_key = STRIPE_SECRET_KEY
```
## Creating a Checkout Session
The modern approach uses Stripe Checkout for a hosted payment page:
```python
# views.py
import stripe
from django.conf import settings
from django.shortcuts import redirect
from django.urls import reverse
def create_checkout_session(request):
cart = request.user.cart
line_items = []
for item in cart.items.all():
line_items.append({
'price_data': {
'currency': 'eur',
'product_data': {
'name': item.product.name,
'description': item.product.short_description[:500],
'images': [request.build_absolute_uri(item.product.image.url)] if item.product.image else [],
},
'unit_amount': int(item.price * 100), # Stripe uses cents
},
'quantity': item.quantity,
})
try:
checkout_session = stripe.checkout.Session.create(
payment_method_types=['card'],
line_items=line_items,
mode='payment',
success_url=request.build_absolute_uri(
reverse('eshop:checkout_success')
) + '?session_id={CHECKOUT_SESSION_ID}',
cancel_url=request.build_absolute_uri(reverse('eshop:cart')),
customer_email=request.user.email,
metadata={
'user_id': str(request.user.id),
'cart_id': str(cart.id),
},
shipping_address_collection={
'allowed_countries': ['US', 'CA', 'GB', 'DE', 'FR', 'NL'],
} if cart.has_physical_items else None,
)
return redirect(checkout_session.url)
except stripe.error.StripeError as e:
messages.error(request, f'Payment error: {str(e)}')
return redirect('eshop:cart')
```
## Handling Webhooks
Webhooks are crucial for reliable payment confirmation:
```python
# views.py
import stripe
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
@csrf_exempt
@require_POST
def stripe_webhook(request):
payload = request.body
sig_header = request.META.get('HTTP_STRIPE_SIGNATURE')
try:
event = stripe.Webhook.construct_event(
payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
)
except ValueError:
return HttpResponse(status=400)
except stripe.error.SignatureVerificationError:
return HttpResponse(status=400)
# Handle specific events
if event['type'] == 'checkout.session.completed':
session = event['data']['object']
handle_successful_payment(session)
elif event['type'] == 'payment_intent.payment_failed':
payment_intent = event['data']['object']
handle_failed_payment(payment_intent)
elif event['type'] == 'customer.subscription.updated':
subscription = event['data']['object']
handle_subscription_update(subscription)
return HttpResponse(status=200)
def handle_successful_payment(session):
"""Process successful payment"""
from .models import Order, OrderItem
user_id = session['metadata']['user_id']
user = User.objects.get(id=user_id)
cart = user.cart
# Create order
order = Order.objects.create(
user=user,
stripe_session_id=session['id'],
stripe_payment_intent=session['payment_intent'],
total=session['amount_total'] / 100,
status='completed',
payment_status='paid',
)
# Create order items
for cart_item in cart.items.all():
OrderItem.objects.create(
order=order,
product=cart_item.product,
quantity=cart_item.quantity,
price=cart_item.price,
)
# Generate license keys
if cart_item.product.requires_license:
generate_license(order, cart_item.product, user)
# Clear cart
cart.items.all().delete()
# Send confirmation email
send_order_confirmation.delay(order.id)
```
## Frontend Integration
Add Stripe.js for card elements:
```html
<!-- checkout.html -->
<script src="https://js.stripe.com/v3/"></script>
<form id="payment-form">
<div id="card-element"></div>
<div id="card-errors" role="alert"></div>
<button type="submit" id="submit-btn">
<span id="button-text">Pay €{{ total }}</span>
<span id="spinner" class="d-none">
<i class="fas fa-spinner fa-spin"></i>
</span>
</button>
</form>
<script>
const stripe = Stripe('{{ stripe_public_key }}');
const elements = stripe.elements();
const style = {
base: {
color: '#32325d',
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
fontSmoothing: 'antialiased',
fontSize: '16px',
'::placeholder': {
color: '#aab7c4'
}
},
invalid: {
color: '#fa755a',
iconColor: '#fa755a'
}
};
const cardElement = elements.create('card', {style: style});
cardElement.mount('#card-element');
// Handle errors
cardElement.on('change', function(event) {
const displayError = document.getElementById('card-errors');
if (event.error) {
displayError.textContent = event.error.message;
} else {
displayError.textContent = '';
}
});
// Handle form submission
const form = document.getElementById('payment-form');
form.addEventListener('submit', async (event) => {
event.preventDefault();
const submitBtn = document.getElementById('submit-btn');
submitBtn.disabled = true;
document.getElementById('spinner').classList.remove('d-none');
const {error, paymentIntent} = await stripe.confirmCardPayment(
clientSecret,
{
payment_method: {
card: cardElement,
billing_details: {
email: '{{ user.email }}'
}
}
}
);
if (error) {
document.getElementById('card-errors').textContent = error.message;
submitBtn.disabled = false;
document.getElementById('spinner').classList.add('d-none');
} else {
window.location.href = '/checkout/success/?payment_intent=' + paymentIntent.id;
}
});
</script>
```
## Subscription Billing
For SaaS products with recurring payments:
```python
def create_subscription(request, price_id):
"""Create a subscription for SaaS products"""
# Get or create Stripe customer
if not request.user.stripe_customer_id:
customer = stripe.Customer.create(
email=request.user.email,
metadata={'user_id': str(request.user.id)}
)
request.user.stripe_customer_id = customer.id
request.user.save()
# Create subscription
subscription = stripe.Subscription.create(
customer=request.user.stripe_customer_id,
items=[{'price': price_id}],
payment_behavior='default_incomplete',
expand=['latest_invoice.payment_intent'],
metadata={
'user_id': str(request.user.id),
}
)
return JsonResponse({
'subscriptionId': subscription.id,
'clientSecret': subscription.latest_invoice.payment_intent.client_secret,
})
```
## Error Handling
Always handle Stripe errors gracefully:
```python
try:
charge = stripe.Charge.create(...)
except stripe.error.CardError as e:
# Card declined
return handle_card_error(e)
except stripe.error.RateLimitError:
# Too many requests
return retry_with_backoff()
except stripe.error.InvalidRequestError as e:
# Invalid parameters
logger.error(f'Invalid Stripe request: {e}')
return handle_invalid_request(e)
except stripe.error.AuthenticationError:
# API key issues
logger.critical('Stripe authentication failed')
return handle_auth_error()
except stripe.error.APIConnectionError:
# Network issues
return handle_network_error()
except stripe.error.StripeError as e:
# Generic error
logger.error(f'Stripe error: {e}')
return handle_generic_error(e)
```
## Testing
Use Stripe's test cards:
| Card Number | Result |
|-------------|--------|
| 4242 4242 4242 4242 | Success |
| 4000 0000 0000 9995 | Declined |
| 4000 0025 0000 3155 | Requires 3D Secure |
## Best Practices
1. **Always use webhooks** - Don't rely on redirect success
2. **Store payment intent IDs** - For refunds and disputes
3. **Implement idempotency** - Prevent duplicate charges
4. **Log everything** - Debug payment issues
5. **Use test mode** - Thoroughly test before going live
Check out our e-commerce templates with Stripe already integrated!