Introduction
Payment processing is the backbone of any e-commerce or fintech application. Whether you’re building a simple checkout flow or a complex marketplace with seller payouts, understanding payment infrastructure is essential.
In this guide, we’ll explore the leading payment processors (Stripe, Adyen), marketplace payment patterns, compliance requirements, and implementation best practices.
Understanding Payment Processing
The Payment Ecosystem
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PAYMENT PROCESSING FLOW โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโ โ
โ โ Customerโโโโโโบโ Merchant โโโโโโบโ Payment โโโโโโบโ Bank โ โ
โ โ โ โ Website โ โ Gateway โ โ Networkโ โ
โ โโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโ โ
โ โ
โ Components: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ โ
โ โ Customer โ Frontend (Checkout) โ โ
โ โ โ โ โ
โ โ Merchant โ Backend (API) โ โ
โ โ โ โ โ
โ โ Payment โ Stripe, Adyen, Braintree โ โ
โ โ Gateway โ โ
โ โ โ โ โ
โ โ Card โ Visa, Mastercard, Amex โ โ
โ โ Network โ โ
โ โ โ โ โ
โ โ Issuing/ โ Customer's bank โ โ
โ โ Acquiring โ โ
โ โ Bank โ โ
โ โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Key Terms
- Merchant: Business selling goods/services
- Cardholder: Customer making payment
- Issuing Bank: Customer’s bank (card issuer)
- Acquiring Bank: Merchant’s bank (receives funds)
- Card Network: Visa, Mastercard, Amex (facilitates)
- Payment Gateway: Processes card transactions
- PCI DSS: Security standard for card data
Stripe: Developer-Friendly Payments
Overview
Stripe is known for its developer-friendly APIs, comprehensive documentation, and extensive features. It’s ideal for startups and scaling companies.
Core Concepts
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ STRIPE CORE FLOW โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ 1. Create Customer โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ stripe.customers.create({ โ โ
โ โ email: "[email protected]", โ โ
โ โ name: "John Doe" โ โ
โ โ metadata: { user_id: "123" } โ โ
โ โ }) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ 2. Create Payment Intent โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ stripe.paymentIntents.create({ โ โ
โ โ amount: 2999, // in cents โ โ
โ โ currency: "usd", โ โ
โ โ customer: "cus_...", โ โ
โ โ payment_method: "pm_...", โ โ
โ โ confirm: true, โ โ
โ โ return_url: "https://..." โ โ
โ โ }) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ 3. Handle 3D Secure (if required) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ { โ โ
โ โ status: "requires_action", โ โ
โ โ next_action: { โ โ
โ โ type: "use_stripe_sdk", โ โ
โ โ stripe_sdk: { ... } โ โ
โ โ } โ โ
โ โ } โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Implementation Examples
import stripe
from stripe import StripeClient
class StripePayments:
"""
Stripe payment integration
"""
def __init__(self, api_key: str):
stripe.api_key = api_key
self.webhook_secret = os.getenv("STRIPE_WEBHOOK_SECRET")
def create_customer(self, email: str, name: str, metadata: dict = None):
"""
Create a Stripe customer
"""
customer = stripe.Customer.create(
email=email,
name=name,
metadata=metadata or {}
)
return {
"customer_id": customer.id,
"email": customer.email,
"created": customer.created
}
def create_payment_intent(
self,
amount: int,
currency: str,
customer_id: str = None,
metadata: dict = None,
capture_method: str = "automatic"
):
"""
Create a payment intent for checkout
"""
intent_params = {
"amount": amount, # Amount in smallest currency unit
"currency": currency.lower(),
"payment_method_types": [
"card", # Credit/debit cards
# Add more:
# "alipay", # Alipay
# "wechat_pay", # WeChat Pay
# "bancontact", # Bancontact
# "ideal", # iDEAL
],
"capture_method": capture_method, # "automatic" or "manual"
"metadata": metadata or {}
}
if customer_id:
intent_params["customer"] = customer_id
payment_intent = stripe.PaymentIntent.create(**intent_params)
return {
"client_secret": payment_intent.client_secret,
"payment_intent_id": payment_intent.id,
"status": payment_intent.status
}
def confirm_payment(self, payment_intent_id: str, payment_method_id: str):
"""
Confirm a payment intent
"""
payment_intent = stripe.PaymentIntent.confirm(
payment_intent_id,
payment_method=payment_method_id
)
return self._format_payment_result(payment_intent)
def handle_3ds_authentication(self, payment_intent_id: str):
"""
Handle 3D Secure authentication
"""
payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id)
if payment_intent.status == "requires_action":
return {
"requires_action": True,
"client_secret": payment_intent.client_secret,
"next_action": payment_intent.next_action
}
return {
"requires_action": False,
"status": payment_intent.status
}
def refund_payment(self, payment_intent_id: str, amount: int = None):
"""
Refund a payment
"""
refund_params = {"payment_intent": payment_intent_id}
if amount:
refund_params["amount"] = amount # Partial refund
refund = stripe.Refund.create(**refund_params)
return {
"refund_id": refund.id,
"status": refund.status,
"amount": refund.amount
}
def create_subscription(
self,
customer_id: str,
price_id: str,
payment_behavior: str = "default_incomplete"
):
"""
Create a subscription
"""
subscription = stripe.Subscription.create(
customer=customer_id,
items=[{"price": price_id}],
payment_behavior=payment_behavior,
expand=["latest_invoice.payment_intent"]
)
return {
"subscription_id": subscription.id,
"status": subscription.status,
"client_secret": (
subscription.latest_invoice.payment_intent.client_secret
if subscription.latest_invoice.payment_intent
else None
)
}
def _format_payment_result(self, payment_intent):
"""
Format payment intent to standardized response
"""
return {
"payment_intent_id": payment_intent.id,
"status": payment_intent.status,
"amount": payment_intent.amount,
"currency": payment_intent.currency,
"succeeded": payment_intent.status == "succeeded"
}
def construct_webhook_event(self, payload: bytes, signature: str):
"""
Verify and construct webhook event
"""
return stripe.Webhook.construct_event(
payload,
signature,
self.webhook_secret
)
Stripe Elements (Frontend)
// Stripe Elements for React
import { loadStripe } from '@stripe/stripe-js';
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
const stripePromise = loadStripe('pk_test_...');
function CheckoutForm({ clientSecret }) {
const stripe = useStripe();
const elements = useElements();
const [error, setError] = useState(null);
const [processing, setProcessing] = useState(false);
const handleSubmit = async (event) => {
event.preventDefault();
if (!stripe || !elements) return;
setProcessing(true);
const { error: submitError } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: 'https://your-site.com/success',
},
});
if (submitError) {
setError(submitError.message);
setProcessing(false);
}
// If successful, Stripe redirects to return_url
};
return (
<form onSubmit={handleSubmit}>
<PaymentElement />
<button disabled={!stripe || processing}>
{processing ? 'Processing...' : 'Pay Now'}
</button>
{error && <div className="error">{error}</div>}
</form>
);
}
function App() {
// Get clientSecret from your backend
const [clientSecret, setClientSecret] = useState('');
useEffect(() => {
fetch('/create-payment-intent', { method: 'POST' })
.then(res => res.json())
.then(data => setClientSecret(data.clientSecret));
}, []);
return (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm />
</Elements>
);
}
Adyen: Enterprise Payments
Overview
Adyen is favored by large enterprises and global companies due to its unified platform, extensive acquiring network, and advanced features.
Key Differences: Stripe vs Adyen
| Feature | Stripe | Adyen |
|---|---|---|
| Target | Startups to mid-market | Enterprise, enterprise+ |
| Pricing | Simple, volume discounts | Custom negotiation |
| Acquiring | Third-party | Direct (own acquiring) |
| Currencies | 135+ | 135+ |
| Local Payment Methods | Many | Extensive |
| Payouts | Stripe Connect | Adyen for Platforms |
| Enterprise Features | Custom accounts | Dedicated support |
Adyen Implementation
from adyen import Adyen
from adyen.services import checkout_api
class AdyenPayments:
"""
Adyen payment integration
"""
def __init__(self, api_key: str, merchant_account: str, environment: str = "test"):
self.adyen = Adyen()
self.adyen.payment.appengine_apikey = api_key
self.merchant_account = merchant_account
self.environment = environment
def create_payment_session(
self,
amount: int,
currency: str,
return_url: str,
reference: str,
metadata: dict = None
):
"""
Create a payment session (Adyen's equivalent to PaymentIntent)
"""
request = {
"merchantAccount": self.merchant_account,
"amount": {
"value": amount,
"currency": currency
},
"returnUrl": return_url,
"reference": reference,
"metadata": metadata or {},
"channel": "Web", # Web, iOS, Android
"countryCode": "US",
"shopperEmail": "[email protected]",
"shopperReference": "user_123"
}
result = self.adyen.checkout.payments_api.payments(request)
return {
"session_id": result.session_data if hasattr(result, 'session_data') else None,
"psp_reference": result.psp_reference if hasattr(result, 'psp_reference') else None,
"result_code": result.result_code if hasattr(result, 'result_code') else None,
"action": result.action if hasattr(result, 'action') else None
}
def handle_redirect_result(self, redirect_result: str):
"""
Handle payment result from redirect
"""
request = {
"merchantAccount": self.merchant_account,
"redirectResult": redirect_result
}
result = self.adyen.checkout.payments_api.paymentsDetails(request)
return {
"psp_reference": result.psp_reference,
"result_code": result.result_code,
"merchant_reference": result.merchant_reference,
"success": result.result_code in ["Authorised", "Received"]
}
def create_payout(
self,
amount: int,
currency: str,
target_reference: str,
target_type: str, # "bankAccount" or "card"
payout_details: dict
):
"""
Create a payout (e.g., to seller in marketplace)
"""
request = {
"merchantAccount": self.merchant_account,
"amount": {
"value": amount,
"currency": currency
},
"reference": f"payout_{int(time.time())}",
"target": {
"type": target_type,
**payout_details
}
}
result = self.adyen.payouts_api.payout(request)
return {
"psp_reference": result.psp_reference,
"result_code": result.result_code,
"status": result.status if hasattr(result, 'status') else None
}
def capture_payment(self, psp_reference: str, amount: int, currency: str):
"""
Capture an authorized payment
"""
request = {
"merchantAccount": self.merchant_account,
"modificationAmount": {
"value": amount,
"currency": currency
},
"originalReference": psp_reference
}
result = self.adyen.checkout.modifications_api.capture_authorised_payment(request)
return {
"psp_reference": result.psp_reference,
"response": result.response
}
Marketplace Payments
Overview
Marketplace payments require handling split payments between platforms and sellers, which adds complexity beyond simple checkout.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MARKETPLACE PAYMENT FLOW โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Customer pays $100 for item โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Platform takes 10% commission = $10 โ โ
โ โ Seller receives 90% = $90 โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ PAYMENT FLOW โ โ
โ โ โ โ
โ โ Customer โโโบ Platform โโโบ Stripe/Adyen โโโบ Seller โ โ
โ โ $100 Hold Split Payout โ โ
โ โ $10 โ โ
โ โ $90 โ โ
โ โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Key Components: โ
โ โข Platform account (connected accounts for sellers) โ
โ โข Split payments (application fee) โ
โ โข Delayed payouts (hold funds until delivery) โ
โ โข KYC/AML compliance for sellers โ
โ โข Refund handling โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Stripe Connect Implementation
class MarketplacePayments:
"""
Stripe Connect for marketplace payments
"""
def __init__(self, api_key: str):
stripe.api_key = api_key
def create_seller_account(self, seller_email: str, business_type: str = "individual"):
"""
Create a connected account for a seller
"""
account = stripe.Account.create(
type="express", # express, standard, or custom
email=seller_email,
business_type=business_type,
capabilities={
"transfers": {"requested": True}
}
)
return {
"account_id": account.id,
"email": account.email,
"charges_enabled": account.charges_enabled,
"payouts_enabled": account.payouts_enabled
}
def create_seller_onboarding_link(self, account_id: str, return_url: str, refresh_url: str):
"""
Create onboarding link for seller
"""
account_link = stripe.AccountLink.create(
account=account_id,
refresh_url=refresh_url,
return_url=return_url,
type="account_onboarding"
)
return {
"url": account_link.url,
"expires_at": account_link.expires_at
}
def create_marketplace_payment(
self,
amount: int,
currency: str,
seller_account_id: str,
application_fee_percent: float,
customer_id: str = None,
metadata: dict = None
):
"""
Create payment with split to seller
"""
# Calculate application fee (platform commission)
application_fee = int(amount * (application_fee_percent / 100))
payment_params = {
"amount": amount,
"currency": currency,
"payment_method_types": ["card"],
"transfer_data": {
"destination": seller_account_id # Seller receives funds
},
"application_fee_amount": application_fee, # Platform keeps this
"metadata": metadata or {}
}
if customer_id:
payment_params["customer"] = customer_id
payment_intent = stripe.PaymentIntent.create(**payment_params)
return {
"client_secret": payment_intent.client_secret,
"payment_intent_id": payment_intent.id,
"application_fee": application_fee,
"seller_amount": amount - application_fee
}
def create_direct_charge(
self,
amount: int,
currency: str,
seller_account_id: str,
connected_payment_method_id: str,
application_fee_amount: int
):
"""
Charge directly on seller's behalf (for marketplace model)
"""
payment_params = {
"amount": amount,
"currency": currency,
"payment_method": connected_payment_method_id,
"confirm": True,
"transfer_data": {
"destination": seller_account_id
},
"application_fee_amount": application_fee_amount
}
payment_intent = stripe.PaymentIntent.create(**payment_params)
return {
"payment_intent_id": payment_intent.id,
"status": payment_intent.status,
"seller_amount": amount - application_fee_amount
}
def create_payout_to_seller(
self,
seller_account_id: str,
amount: int,
currency: str,
reference: str = None
):
"""
Create manual payout to seller
"""
transfer = stripe.Transfer.create(
amount=amount,
currency=currency,
destination=seller_account_id,
description=reference or "Marketplace payout"
)
return {
"transfer_id": transfer.id,
"amount": transfer.amount,
"destination": transfer.destination,
"created": transfer.created
}
def handle_marketplace_refund(
self,
payment_intent_id: str,
refund_amount: int = None,
refund_application_fee: bool = True
):
"""
Handle refund in marketplace
"""
refund_params = {
"payment_intent": payment_intent_id,
"refund_application_fee": refund_application_fee
}
if refund_amount:
refund_params["amount"] = refund_amount
refund = stripe.Refund.create(**refund_params)
return {
"refund_id": refund.id,
"status": refund.status,
"amount": refund.amount
}
Strong Customer Authentication (SCA)
Overview
PSD2 mandates Strong Customer Authentication (SCA) for online payments in Europe. This requires two or more factors from:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ STRONG CUSTOMER AUTHENTICATION (SCA) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Two or more independent factors: โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โ
โ โ Knowledge โ โ Possession โ โ Inherence โ โ
โ โ (something you โ โ (something you โ โ (something you โ โ
โ โ know) โ โ have) โ โ are) โ โ
โ โโโโโโโโโโโโโโโโโโโโค โโโโโโโโโโโโโโโโโโโโค โโโโโโโโโโโโโโโโโโโโค โ
โ โ โข Password โ โ โข Phone โ โ โข Fingerprint โ โ
โ โ โข PIN โ โ โข Card reader โ โ โข Face ID โ โ
โ โ โข Security Q โ โ โข Hardware tokenโ โ โข Voice ID โ โ
โ โ โ โ โข Email code โ โ โข Retina scan โ โ
โ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ SCA Required: Card payments, account access โ
โ Exceptions: Low risk, trusted merchants, recurring payments โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Implementing SCA with Stripe
class SCAPayments:
"""
Handle Strong Customer Authentication
"""
def __init__(self, api_key: str):
stripe.api_key = api_key
def check_if_sca_required(self, payment_intent_id: str) -> dict:
"""
Check if payment requires SCA
"""
payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id)
# Check payment status
if payment_intent.status == "requires_action":
# SCA is required - 3D Secure
return {
"sca_required": True,
"authentication": payment_intent.next_action
}
return {"sca_required": False}
def handle_sca_verification(self, payment_intent_id: str):
"""
Create SetupIntent for future authentication
"""
# For storing payment method with SCA
setup_intent = stripe.SetupIntent.create(
usage="off_session", # Use without customer present
payment_method_types=["card"]
)
return {
"client_secret": setup_intent.client_secret,
"setup_intent_id": setup_intent.id
}
def use_existing_payment_method(self, customer_id: str, payment_method_id: str):
"""
Use stored payment method (may trigger SCA)
"""
payment_intent = stripe.PaymentIntent.create(
amount=2999,
currency="eur",
customer=customer_id,
payment_method=payment_method_id,
confirm=True,
off_session=True, # Customer not present
# Stripe will handle SCA automatically
)
if payment_intent.status == "requires_action":
# Need to complete authentication
return {
"requires_action": True,
"client_secret": payment_intent.client_secret
}
return {
"requires_action": False,
"succeeded": payment_intent.status == "succeeded"
}
def request_exemption(self, payment_intent_id: str, exemption_type: str):
"""
Request SCA exemption (suitable cases only)
"""
# Valid exemption types:
# - low_value: Payments under โฌ30
# - trusted_beneficiary: Trusted merchants
# - corporate: Corporate card payments
# - recurring: Subsequent payments in series
payment_intent = stripe.PaymentIntent.modify(
payment_intent_id,
payment_method_options={
"card": {
"request_three_d_secure": "automatic"
}
}
)
return {"exemption_requested": exemption_type}
Common Pitfalls
1. Not Handling Webhooks Properly
# Anti-pattern: No webhook handling
def bad_webhook_handling():
"""
Anti-pattern: Ignoring webhooks
"""
# Only checking payment status on page load
# Result: Missed payment confirmations, lost revenue
return "This approach is unreliable"
# Good pattern: Robust webhook handling
def good_webhook_handling():
"""
Handle all payment events reliably
"""
from flask import Flask, request
app = Flask(__name__)
@app.route('/webhooks/stripe', methods=['POST'])
def handle_stripe_webhook():
payload = request.get_data()
sig_header = request.headers.get('stripe-signature')
try:
event = stripe.Webhook.construct_event(
payload, sig_header, WEBHOOK_SECRET
)
except ValueError:
return 'Invalid payload', 400
except stripe.error.SignatureVerificationError:
return 'Invalid signature', 400
# Handle specific events
event_handlers = {
'payment_intent.succeeded': handle_payment_success,
'payment_intent.payment_failed': handle_payment_failure,
'charge.refunded': handle_refund,
'customer.subscription.created': handle_subscription_created,
'customer.subscription.deleted': handle_subscription_cancelled,
'invoice.payment_failed': handle_invoice_failed,
}
handler = event_handlers.get(event['type'])
if handler:
handler(event['data']['object'])
return '', 200
return app
2. Not Implementing Idempotency
# Anti-pattern: No idempotency
def bad_idempotency():
"""
Anti-pattern: Duplicate charges on retry
"""
# User clicks "Pay" twice, network fails, user retries
# Result: Multiple charges!
return "Dangerous: Can cause duplicate charges"
# Good pattern: Idempotent requests
def good_idempotency():
"""
Use idempotency keys to prevent duplicate charges
"""
import uuid
def create_idempotent_payment(amount: int, customer_id: str):
"""
Create payment with idempotency key
"""
# Generate unique key per payment attempt
idempotency_key = f"{customer_id}_{amount}_{uuid.uuid4()}"
# Include in request
payment_intent = stripe.PaymentIntent.create(
amount=amount,
currency="usd",
customer=customer_id,
idempotency_key=idempotency_key
)
# If same key is used, Stripe returns same result
# No duplicate charges!
return payment_intent
3. Ignoring Payment Failures
# Anti-pattern: Not handling declined cards
def bad_failure_handling():
"""
Anti-pattern: Generic error message
"""
try:
# Process payment
pass
except Exception as e:
# What type of failure?
return "Payment failed" # Not helpful!
# Good pattern: Specific error handling
def good_failure_handling():
"""
Handle different failure types specifically
"""
def handle_payment_error(error):
error_code = error.get("code")
# Map of actionable error messages
error_messages = {
"card_declined": {
"message": "Your card was declined. Please try another card.",
"action": "request_new_card"
},
"expired_card": {
"message": "Your card has expired. Please use a different card.",
"action": "request_new_card"
},
"incorrect_cvc": {
"message": "The security code is incorrect. Please check and try again.",
"action": "request_cvc"
},
"insufficient_funds": {
"message": "Insufficient funds. Please try another card.",
"action": "request_new_card"
},
"processing_error": {
"message": "A processing error occurred. Please try again.",
"action": "retry"
},
"lost_card": {
"message": "This card has been reported lost. Please use a different card.",
"action": "request_new_card"
},
"stolen_card": {
"message": "This card has been reported stolen. Please contact your bank.",
"action": "request_new_card"
}
}
error_info = error_messages.get(
error_code,
{"message": "An error occurred. Please try again.", "action": "support"}
)
return error_info
Best Practices
1. Always Use Test Mode First
# Use Stripe test card numbers
TEST_CARDS = {
"success": "4242424242424242",
"declined": "4000000000000002",
"insufficient_funds": "4000000000009995",
"expired_card": "4000000000000069",
"incorrect_cvc": "4000000000000127",
"processing_error": "4000000000000119",
# 3D Secure
"3d_success": "4000000000003220",
"3d_failure": "4000000000003246",
"3d_issuer_unavailable": "4000000000003055"
}
# Test different scenarios
def test_payment_scenarios():
# Test successful payment
# Test declined card
# Test 3D Secure flow
# Test refund flow
# Test webhook delivery
pass
2. Store Only What You Need
# Minimize PCI DSS scope
# Don't store card details on your servers!
# Good pattern: Use Stripe's tokenization
def good_tokenization():
"""
Let Stripe handle card data
"""
# Frontend: Stripe.js collects card details
# Backend: Only receives token or payment method ID
# Never touches raw card numbers
return "Low PCI compliance burden"
# Anti-pattern: Storing card numbers
def bad_storage():
"""
Anti-pattern: Storing card numbers (high PCI scope!)
"""
# NEVER store: card number, CVV, PIN
# Only store: last 4 digits, expiry, card brand
pass
3. Handle Currencies Properly
def handle_currencies():
"""
Currency handling best practices
"""
# Always use smallest currency unit (cents, pence)
# Don't use floats for money!
# Good: 2999 = $29.99
# Bad: 29.99 (floating point errors!)
from decimal import Decimal
def dollars_to_cents(dollars: float) -> int:
"""Convert dollars to cents"""
return int(Decimal(str(dollars)) * 100)
def cents_to_dollars(cents: int) -> float:
"""Convert cents to dollars"""
return float(Decimal(cents) / 100)
External Resources
- Stripe Documentation
- Adyen Documentation
- Stripe Connect Guide
- PCI DSS Compliance
- PSD2 SCA Requirements
- 3D Secure 2
Conclusion
Payment processing is complex but critical for any business handling transactions. Whether you choose Stripe for its developer experience or Adyen for enterprise capabilities, understanding the fundamentals of payment flows, marketplace patterns, and compliance requirements will help you build robust financial systems.
Key takeaways:
- Stripe is ideal for startups and mid-market; Adyen for enterprises
- Marketplace payments require connected accounts and split payments
- SCA (Strong Customer Authentication) is mandatory in Europe
- Always use idempotency keys to prevent duplicate charges
- Handle webhooks reliably for payment status updates
- Minimize PCI DSS scope by using tokenization
- Use webhooks instead of polling for payment status
Building a robust payment system requires careful attention to edge cases, security, and compliance. Take advantage of the extensive documentation and testing tools provided by payment processors to ensure your implementation is production-ready.
Comments