This guide will walk you through integrating Stripe payment processing into your web application. Whether you're building an e-commerce platform, subscription service, or marketplace, you'll learn how to accept payments securely and reliably.
What you'll build:
Estimated time: 2-3 hours for basic integration
Prerequisites:
Understanding the payment flow helps you build a secure integration:
This architecture keeps sensitive payment data off your servers and ensures PCI compliance.
Stripe provides two sets of keys:
To find your keys:
Important: The publishable key is safe to expose in your frontend code. The secret key must be kept secure on your backend server—never expose it in client-side code.
Install the Stripe SDK for your backend and frontend:
Node.js:
npm install stripe
Python:
pip install stripe
Ruby:
gem install stripe
PHP:
composer require stripe/stripe-php
Add Stripe.js to your HTML (no installation needed):
<script src="https://js.stripe.com/v3/"></script>
Create a simple checkout form that collects payment information securely.
<!DOCTYPE html>
<html>
<head>
<title>Checkout</title>
<script src="https://js.stripe.com/v3/"></script>
<style>
.checkout-form {
max-width: 500px;
margin: 50px auto;
padding: 30px;
border: 1px solid #ddd;
border-radius: 8px;
}
#card-element {
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 20px;
}
#card-errors {
color: #dc3545;
margin-bottom: 20px;
}
.submit-button {
width: 100%;
padding: 15px;
background: #635bff;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
}
.submit-button:hover {
background: #4f46e5;
}
.submit-button:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="checkout-form">
<h2>Complete Your Purchase</h2>
<p>Total: $49.99</p>
<form id="payment-form">
<div id="card-element"></div>
<div id="card-errors"></div>
<button type="submit" class="submit-button">Pay $49.99</button>
</form>
</div>
<script src="checkout.js"></script>
</body>
</html>
// Initialize Stripe with your publishable key
const stripe = Stripe('pk_test_your_publishable_key_here');
// Create card element
const elements = stripe.elements();
const cardElement = elements.create('card', {
style: {
base: {
fontSize: '16px',
color: '#32325d',
'::placeholder': {
color: '#aab7c4',
},
},
},
});
// Mount card element to the page
cardElement.mount('#card-element');
// Handle real-time validation errors
cardElement.on('change', (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();
// Disable the submit button to prevent duplicate submissions
const submitButton = form.querySelector('button');
submitButton.disabled = true;
submitButton.textContent = 'Processing...';
try {
// Create payment method
const {error, paymentMethod} = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
});
if (error) {
// Show error to customer
document.getElementById('card-errors').textContent = error.message;
submitButton.disabled = false;
submitButton.textContent = 'Pay $49.99';
return;
}
// Send payment method to your server
const response = await fetch('/create-payment-intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
payment_method_id: paymentMethod.id,
amount: 4999, // Amount in cents
}),
});
const data = await response.json();
if (data.error) {
document.getElementById('card-errors').textContent = data.error;
submitButton.disabled = false;
submitButton.textContent = 'Pay $49.99';
return;
}
// Payment succeeded
window.location.href = '/success?payment_intent=' + data.payment_intent_id;
} catch (err) {
console.error('Payment error:', err);
document.getElementById('card-errors').textContent =
'An unexpected error occurred. Please try again.';
submitButton.disabled = false;
submitButton.textContent = 'Pay $49.99';
}
});
Your server needs to create a payment intent and confirm the payment.
const express = require('express');
const stripe = require('stripe')('sk_test_your_secret_key_here');
const app = express();
app.use(express.json());
app.post('/create-payment-intent', async (req, res) => {
try {
const { payment_method_id, amount } = req.body;
// Validate amount (always validate on the server!)
if (!amount || amount < 50) {
return res.status(400).json({
error: 'Invalid amount'
});
}
// Create a payment intent
const paymentIntent = await stripe.paymentIntents.create({
amount: amount,
currency: 'usd',
payment_method: payment_method_id,
confirm: true,
automatic_payment_methods: {
enabled: true,
allow_redirects: 'never',
},
metadata: {
order_id: 'order_12345', // Your internal order ID
customer_email: 'customer@example.com',
},
});
res.json({
success: true,
payment_intent_id: paymentIntent.id,
});
} catch (error) {
console.error('Payment error:', error);
res.status(400).json({
error: error.message
});
}
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
from flask import Flask, request, jsonify
import stripe
stripe.api_key = 'sk_test_your_secret_key_here'
app = Flask(__name__)
@app.route('/create-payment-intent', methods=['POST'])
def create_payment():
try:
data = request.get_json()
payment_method_id = data.get('payment_method_id')
amount = data.get('amount')
# Validate amount
if not amount or amount < 50:
return jsonify({'error': 'Invalid amount'}), 400
# Create payment intent
payment_intent = stripe.PaymentIntent.create(
amount=amount,
currency='usd',
payment_method=payment_method_id,
confirm=True,
automatic_payment_methods={
'enabled': True,
'allow_redirects': 'never',
},
metadata={
'order_id': 'order_12345',
'customer_email': 'customer@example.com',
},
)
return jsonify({
'success': True,
'payment_intent_id': payment_intent.id,
})
except stripe.error.StripeError as e:
return jsonify({'error': str(e)}), 400
except Exception as e:
return jsonify({'error': 'An error occurred'}), 500
if __name__ == '__main__':
app.run(port=3000)
Webhooks ensure you're notified when payments succeed, even if the user closes their browser before the confirmation page loads.
Scenario: A customer completes payment but their internet disconnects before your success page loads. Without webhooks, you'd never know the payment succeeded, and the customer wouldn't receive their order.
Solution: Stripe sends webhooks to your server independent of the user's browser, ensuring reliable order fulfillment.
Node.js:
const endpointSecret = 'whsec_your_webhook_secret_here';
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
// Verify webhook signature
event = stripe.webhooks.constructEvent(
req.body,
sig,
endpointSecret
);
} catch (err) {
console.log(`Webhook signature verification failed: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
console.log('Payment succeeded:', paymentIntent.id);
// Fulfill the order
fulfillOrder(paymentIntent.metadata.order_id);
break;
case 'payment_intent.payment_failed':
const failedPayment = event.data.object;
console.log('Payment failed:', failedPayment.id);
// Notify customer of failure
notifyCustomer(failedPayment.metadata.customer_email);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.json({received: true});
});
function fulfillOrder(orderId) {
// Your order fulfillment logic
console.log(`Fulfilling order: ${orderId}`);
// Send email, update database, ship product, etc.
}
function notifyCustomer(email) {
// Notify customer of payment failure
console.log(`Notifying customer: ${email}`);
}
https://yourdomain.com/webhookpayment_intent.succeeded, payment_intent.payment_failedUse the Stripe CLI to forward webhooks to your local development server:
# Install Stripe CLI
brew install stripe/stripe-cli/stripe # macOS
# or download from stripe.com/docs/stripe-cli
# Login
stripe login
# Forward webhooks to localhost
stripe listen --forward-to localhost:3000/webhook
DON'T:
// NEVER DO THIS
const cardData = {
number: req.body.card_number,
cvc: req.body.cvc,
// Never handle raw card data
};
DO:
// Always use Stripe.js on the frontend
// Card data goes directly to Stripe, never touches your server
const {paymentMethod} = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
});
Critical: Never trust the amount sent from the frontend.
// BAD: Using amount directly from frontend
const amount = req.body.amount; // User could change this!
// GOOD: Look up the real price from your database
const product = await getProductFromDatabase(req.body.product_id);
const amount = product.price;
Stripe requires HTTPS for all payment-related pages. Use a free SSL certificate from Let's Encrypt or your hosting provider.
Prevent duplicate payments if a request is retried:
const paymentIntent = await stripe.paymentIntents.create({
amount: amount,
currency: 'usd',
payment_method: payment_method_id,
}, {
idempotencyKey: orderId, // Prevents duplicate charges
});
Always verify that webhooks actually come from Stripe:
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
endpointSecret
);
} catch (err) {
// Invalid signature - reject the webhook
return res.status(400).send('Invalid signature');
}
Stripe provides test cards for different scenarios:
4242 4242 4242 42424000 0025 0000 31554000 0000 0000 99954000 0000 0000 9995Use any future expiration date and any 3-digit CVC.
4242 4242 4242 42424000 0000 0000 0002Before accepting real payments, verify:
Cause: Payment method ID not created or expired.
Solution: Ensure you're creating the payment method with stripe.createPaymentMethod() before sending it to your server.
Cause: Using wrong signing secret or webhook body was modified.
Solution:
express.raw() middleware (not express.json())Cause: User clicks "Pay" button multiple times or network retry.
Solution: Implement idempotency keys and disable submit button after first click.
Cause: Webhook not received or webhook handler crashed.
Solution:
Questions about this integration guide? Reach out to discuss your specific implementation needs.