Complete Braintree Integration for Django: Production-Ready Payment Processing
A comprehensive guide to implementing Braintree payments in Django covering sandbox setup, security best practices, webhooks, and real-world production considerations.
Why Braintree for Django Applications?
After implementing payment systems across multiple Django projects, Braintree consistently emerges as the optimal choice for developers needing enterprise-grade features with developer-friendly APIs. Unlike simpler payment processors, Braintree provides:
Multi-Payment Method Support
- Credit/Debit Cards (Visa, Mastercard, Amex)
- Digital Wallets (PayPal, Venmo, Apple Pay)
- Crypto (Bitcoin via Coinbase)
- Bank Transfers (ACH, SEPA)
Django-Specific Advantages
- Python-first SDK with Django patterns
- Built-in PCI compliance handling
- Seamless integration with Django auth/user models
- Production-ready error handling
Business Benefits
- Unified reporting across payment methods
- Advanced fraud protection (Kount)
- Recurring billing/subscription management
- Global currency support (130+ currencies)
Real Implementation Context: This guide is based on production experience from WikiPro.us, a Django-based knowledge platform requiring multi-tier subscription plans with global payment support.
System Architecture: The Three-Tier Approach
Layer 1: Client-Side (Frontend)
Braintree Drop-in UI → Token Generation → Django Templates
Responsibilities:
- Secure payment form rendering
- Client-side validation
- Payment method nonce generation
- Error feedback to users
Layer 2: Application Layer (Django Views)
Request Handling → Braintree API Calls → Business Logic
Responsibilities:
- Client token generation
- Transaction processing
- Customer vault management
- Security validation
Layer 3: Infrastructure (Settings/Configuration)
Environment Management → Key Configuration → Webhook Setup
Responsibilities:
- Sandbox/production environment separation
- API key management
- Webhook endpoint security
- Logging and monitoring setup
Step-by-Step Implementation
Step 1: Account Setup & Environment Configuration
Required Accounts:
- Sandbox Account: Braintree Sandbox - For testing
- Production Account: Braintree Production - Live transactions
Django Settings Configuration:
# settings.py
import sys
from decouple import config # Using python-decouple for env variables
# Environment detection
BRAINTREE_PRODUCTION = not (len(sys.argv) >= 2 and sys.argv[1] == 'runserver')
# API Configuration
BRAINTREE_ENVIRONMENT = braintree.Environment.Production if BRAINTREE_PRODUCTION else braintree.Environment.Sandbox
# Secure credential loading
BRAINTREE_MERCHANT_ID = config('BRAINTREE_MERCHANT_ID')
BRAINTREE_PUBLIC_KEY = config('BRAINTREE_PUBLIC_KEY')
BRAINTREE_PRIVATE_KEY = config('BRAINTREE_PRIVATE_KEY')
# Webhook configuration
BRAINTREE_WEBHOOK_SECRET = config('BRAINTREE_WEBHOOK_SECRET', default='')
# Additional settings for production
BRAINTREE_CONFIG = {
'environment': BRAINTREE_ENVIRONMENT,
'merchant_id': BRAINTREE_MERCHANT_ID,
'public_key': BRAINTREE_PUBLIC_KEY,
'private_key': BRAINTREE_PRIVATE_KEY,
'timeout': 30, # Request timeout in seconds
'fail_on_http_error': True, # Raise exceptions on HTTP errors
}
Step 2: Installation & Initial Setup
# Install Braintree Python SDK
pip install braintree
# Additional recommended packages
pip install python-decouple # Environment variable management
pip install django-environ # Alternative for Django-specific env vars
pip install sentry-sdk # Error tracking for production
Why These Packages:
- python-decouple: Secure credential management away from codebase
- sentry-sdk: Critical for monitoring payment failures in production
- Braintree SDK: Official Python library with Django compatibility
Step 3: Core View Implementation
Payment Flow Controller:
# views/payments.py
import braintree
import logging
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse, HttpResponseBadRequest
from django.shortcuts import render, redirect
logger = logging.getLogger(__name__)
def configure_braintree():
"""Configure Braintree with current settings"""
return braintree.Configuration.configure(
settings.BRAINTREE_ENVIRONMENT,
merchant_id=settings.BRAINTREE_MERCHANT_ID,
public_key=settings.BRAINTREE_PUBLIC_KEY,
private_key=settings.BRAINTREE_PRIVATE_KEY
)
@login_required
def checkout_view(request):
"""
Render checkout page with Braintree client token
"""
configure_braintree()
# Generate client token with customer ID for returning customers
user = request.user
customer_kwargs = {}
if hasattr(user, 'braintree_customer_id') and user.braintree_customer_id:
customer_kwargs = {"customer_id": user.braintree_customer_id}
else:
# First-time customer - generate without ID
customer_kwargs = {}
try:
braintree_client_token = braintree.ClientToken.generate(customer_kwargs)
except braintree.exceptions.NotFoundError:
# Customer doesn't exist in Braintree yet
braintree_client_token = braintree.ClientToken.generate({})
except Exception as e:
logger.error(f"Braintree token generation failed: {e}")
# Fallback token for error state
braintree_client_token = braintree.ClientToken.generate({})
context = {
'braintree_client_token': braintree_client_token,
'user_email': user.email,
'amount': '10.00', # Dynamic based on cart/plan
}
return render(request, 'payments/checkout.html', context)
Key Implementation Details:
- Error Handling: Comprehensive try-except blocks with logging
- Customer Vault: Reuse existing customer IDs for returning users
- Configuration Separation: Isolated Braintree config function
- Logging: Structured logging for debugging production issues
Step 4: Template Integration with Enhanced Security
Checkout Template (checkout.html):
<!-- payments/checkout.html -->
{% extends "base.html" %}
{% load static %}
{% block extra_head %}
<script src="https://js.braintreegateway.com/web/dropin/1.38.0/js/dropin.min.js"></script>
<meta name="csrf-token" content="{{ csrf_token }}">
{% endblock %}
{% block content %}
<div class="payment-container">
<h2>Complete Your Payment</h2>
<div class="payment-errors" id="payment-errors">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
{% endif %}
</div>
<form id="payment-form" method="post" autocomplete="off">
{% csrf_token %}
<div class="form-group">
<label for="amount">Amount</label>
<input type="text" id="amount" value="{{ amount }}" readonly class="form-control">
</div>
<div class="form-group">
<label>Payment Method</label>
<div id="braintree-dropin-container"></div>
</div>
<input type="hidden" id="nonce" name="payment_method_nonce">
<button type="submit" class="btn btn-primary btn-lg btn-block" id="submit-button">
Pay ${{ amount }}
</button>
</form>
</div>
<script>
(function() {
'use strict';
var form = document.querySelector('#payment-form');
var submitButton = document.querySelector('#submit-button');
var csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
// Initialize Braintree Drop-in
braintree.dropin.create({
authorization: '{{ braintree_client_token }}',
container: '#braintree-dropin-container',
card: {
cardholderName: {
required: false
},
overrides: {
fields: {
number: {
placeholder: '4111 1111 1111 1111'
},
cvv: {
placeholder: '123'
}
}
}
},
paypal: {
flow: 'checkout',
amount: '{{ amount }}',
currency: 'USD'
},
venmo: {
allowNewBrowserTab: false
}
}, function (createErr, instance) {
if (createErr) {
console.error('Drop-in creation error:', createErr);
showError('Payment system initialization failed. Please refresh the page.');
return;
}
// Handle form submission
form.addEventListener('submit', function (event) {
event.preventDefault();
submitButton.disabled = true;
submitButton.textContent = 'Processing...';
instance.requestPaymentMethod(function (err, payload) {
if (err) {
submitButton.disabled = false;
submitButton.textContent = 'Pay ${{ amount }}';
showError('Payment method selection failed: ' + err.message);
return;
}
// Add nonce to form and submit
document.querySelector('#nonce').value = payload.nonce;
// AJAX submission
fetch('{% url "process_payment" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
payment_method_nonce: payload.nonce,
amount: '{{ amount }}'
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.href = data.redirect_url;
} else {
showError(data.error || 'Payment failed');
submitButton.disabled = false;
submitButton.textContent = 'Pay ${{ amount }}';
}
})
.catch(error => {
showError('Network error. Please try again.');
submitButton.disabled = false;
submitButton.textContent = 'Pay ${{ amount }}';
});
});
});
});
function showError(message) {
var errorDiv = document.querySelector('#payment-errors');
errorDiv.innerHTML = '<div class="alert alert-danger">' + message + '</div>';
errorDiv.scrollIntoView({ behavior: 'smooth' });
}
})();
</script>
{% endblock %}
Step 5: Payment Processing View
Transaction Handler:
# views/transactions.py
import json
import logging
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse
from django.db import transaction as db_transaction
logger = logging.getLogger(__name__)
@require_POST
@csrf_exempt # Required for Braintree webhooks, use carefully
def process_payment(request):
"""
Process payment from Braintree nonce
"""
try:
data = json.loads(request.body)
nonce = data.get('payment_method_nonce')
amount = data.get('amount', '10.00')
if not nonce:
return JsonResponse({
'success': False,
'error': 'No payment method provided'
}, status=400)
# Configure Braintree
from .payments import configure_braintree
configure_braintree()
user = request.user
# Create or retrieve Braintree customer
customer_result = braintree.Customer.create({
"first_name": user.first_name,
"last_name": user.last_name,
"email": user.email,
"payment_method_nonce": nonce
})
if not customer_result.is_success:
logger.error(f"Customer creation failed: {customer_result.message}")
return JsonResponse({
'success': False,
'error': 'Customer account setup failed'
}, status=400)
customer_id = customer_result.customer.id
# Store customer ID in user profile
user.braintree_customer_id = customer_id
user.save()
# Process transaction
sale_result = braintree.Transaction.sale({
"amount": amount,
"payment_method_nonce": nonce,
"customer_id": customer_id,
"options": {
"submit_for_settlement": True,
"store_in_vault_on_success": True
},
"custom_fields": {
"user_id": str(user.id),
"application": "django_app"
}
})
if sale_result.is_success:
# Record successful transaction in your database
transaction = Transaction.objects.create(
user=user,
braintree_id=sale_result.transaction.id,
amount=amount,
status='settled',
payment_method=sale_result.transaction.payment_instrument_type
)
logger.info(f"Payment successful: {sale_result.transaction.id}")
return JsonResponse({
'success': True,
'transaction_id': sale_result.transaction.id,
'redirect_url': '/payment/success/'
})
else:
logger.error(f"Transaction failed: {sale_result.message}")
# Handle specific error cases
error_message = "Payment failed"
if sale_result.transaction:
if sale_result.transaction.processor_response_code == '2000':
error_message = "Card declined"
elif sale_result.transaction.processor_response_code == '2001':
error_message = "Insufficient funds"
return JsonResponse({
'success': False,
'error': error_message,
'processor_code': sale_result.transaction.processor_response_code if sale_result.transaction else None
}, status=400)
except json.JSONDecodeError:
return JsonResponse({
'success': False,
'error': 'Invalid request data'
}, status=400)
except Exception as e:
logger.exception("Unexpected payment processing error")
return JsonResponse({
'success': False,
'error': 'Internal server error'
}, status=500)
Production-Ready Enhancements
1. Webhook Implementation
Handle asynchronous payment events:
@csrf_exempt
@require_POST
def braintree_webhook(request):
"""
Handle Braintree webhook notifications
"""
signature = request.POST.get('bt_signature')
payload = request.POST.get('bt_payload')
try:
webhook_notification = braintree.WebhookNotification.parse(signature, payload)
if webhook_notification.kind in ['subscription_charged_successfully',
'subscription_charged_unsuccessfully']:
# Handle subscription events
pass
elif webhook_notification.kind == 'dispute_opened':
# Handle disputes
pass
return HttpResponse(status=200)
except Exception as e:
logger.error(f"Webhook processing error: {e}")
return HttpResponse(status=400)
2. Transaction Model
Complete Django model for transaction tracking:
# models/transactions.py
from django.db import models
from django.contrib.auth.models import User
class Transaction(models.Model):
TRANSACTION_STATUS = [
('authorized', 'Authorized'),
('submitted_for_settlement', 'Submitted for Settlement'),
('settling', 'Settling'),
('settled', 'Settled'),
('failed', 'Failed'),
('voided', 'Voided'),
]
user = models.ForeignKey(User, on_delete=models.PROTECT)
braintree_id = models.CharField(max_length=255, unique=True)
amount = models.DecimalField(max_digits=10, decimal_places=2)
currency = models.CharField(max_length=3, default='USD')
status = models.CharField(max_length=50, choices=TRANSACTION_STATUS)
payment_method = models.CharField(max_length=50)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Additional fields for reconciliation
processor_response_code = models.CharField(max_length=10, null=True)
processor_response_text = models.TextField(null=True)
class Meta:
indexes = [
models.Index(fields=['braintree_id']),
models.Index(fields=['user', 'created_at']),
]
def __str__(self):
return f"{self.braintree_id} - {self.amount} {self.currency}"
Comprehensive Testing Strategy
Sandbox Test Cards
| Card Number | Scenario | Expected Result |
|---|---|---|
4111111111111111 |
Successful Visa | Transaction approved |
4000111111111115 |
Declined card | Transaction declined |
378282246310005 |
Successful Amex | Transaction approved |
6011111111111117 |
Successful Discover | Transaction approved |
Django Test Cases
# tests/test_payments.py
from django.test import TestCase
from django.contrib.auth.models import User
from unittest.mock import patch
class PaymentTestCase(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
email='test@example.com'
)
self.client.force_login(self.user)
@patch('braintree.Transaction.sale')
def test_successful_payment(self, mock_sale):
# Mock successful Braintree response
mock_sale.return_value = Mock(
is_success=True,
transaction=Mock(id='test_transaction_123')
)
response = self.client.post('/process-payment/', {
'payment_method_nonce': 'fake-valid-nonce'
})
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(response.content, {
'success': True,
'transaction_id': 'test_transaction_123'
})
def test_missing_nonce(self):
response = self.client.post('/process-payment/', {})
self.assertEqual(response.status_code, 400)
Security & Compliance Checklist
✅ PCI DSS Compliance
- Never store raw card data in your database
- Use Braintree's tokenization system
- Implement TLS 1.2+ for all payment pages
- Regular security audits and penetration testing
✅ Django Security Features
- Enable CSRF protection (except webhook endpoints)
- Use Django's built-in authentication system
- Implement rate limiting on payment endpoints
- Sanitize all user inputs and outputs
✅ Production Monitoring
- Log all payment attempts (success/failure)
- Monitor for unusual transaction patterns
- Set up alerts for failed transactions above threshold
- Regularly update Braintree SDK and dependencies
