Learn how to integrate Stripe API with Node.js: set up your account, process payments, handle webhooks, manage subscriptions, and implement customer management.
Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Introduction
This tutorial will guide you through the process of integrating Stripe's payment processing API with a Node.js application. We'll cover setting up your Stripe account, installing necessary dependencies, implementing basic payment functionality, and handling common use cases.
Step 1: Set Up Your Stripe Account
Before you can start integrating Stripe with Node.js, you need to create and set up your Stripe account:
Note: Stripe provides both test and live keys. Always use test keys during development to avoid processing real payments.
Step 2: Set Up Your Node.js Project
Let's create a new Node.js project and install the necessary dependencies:
mkdir stripe-node-project
cd stripe-node-project
npm init -y
npm install stripe express body-parser dotenv
Create a basic project structure:
touch server.js
touch .env
mkdir public
touch public/index.html
touch public/script.js
touch public/style.css
Step 3: Configure Environment Variables
Create a .env
file to securely store your Stripe API keys:
STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key
STRIPE_SECRET_KEY=sk_test_your_secret_key
Make sure to replace the placeholder values with your actual test API keys from your Stripe dashboard.
Step 4: Set Up a Basic Express Server
Create a basic Express server in your server.js
file:
// Import required dependencies
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const path = require('path');
// Initialize Express app
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware setup
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(path.join(\_\_dirname, 'public')));
// Routes will be added here
// Start the server
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Step 5: Create a Basic Frontend
Let's create a simple payment form in your public/index.html
file:
Stripe Payment Example
Stripe Payment Demo
Payment Result
</div>
Add some basic styles in public/style.css
:
body {
font-family: 'Arial', sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
background-color: #f7f9fc;
}
.container {
max-width: 600px;
margin: 0 auto;
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 10px;
font-weight: bold;
}
#card-element {
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 12px;
background-color: #fafafa;
}
#card-errors {
color: #fa755a;
margin-top: 10px;
font-size: 14px;
}
button {
background-color: #6772e5;
color: white;
border: none;
padding: 12px 16px;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: all 0.2s ease;
}
button:hover {
background-color: #5469d4;
}
button:disabled {
background-color: #b9b9b9;
cursor: not-allowed;
}
.hidden {
display: none;
}
pre {
background-color: #f4f4f4;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
}
Step 6: Implement Frontend JavaScript
Add the client-side JavaScript in public/script.js
:
document.addEventListener('DOMContentLoaded', async () => {
// Fetch the publishable key from our server
const response = await fetch('/config');
const { publishableKey } = await response.json();
// Initialize Stripe.js
const stripe = Stripe(publishableKey);
// Create an instance of Elements
const elements = stripe.elements();
// Create the Card Element and mount it
const cardElement = elements.create('card');
cardElement.mount('#card-element');
// Handle real-time validation errors from the card Element
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');
const submitButton = document.getElementById('submit-button');
form.addEventListener('submit', async (event) => {
event.preventDefault();
// Disable the submit button to prevent repeated clicks
submitButton.disabled = true;
submitButton.textContent = 'Processing...';
try {
// Create a PaymentMethod
const { paymentMethod, error } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
});
if (error) {
// Show error to your customer
const errorElement = document.getElementById('card-errors');
errorElement.textContent = error.message;
submitButton.disabled = false;
submitButton.textContent = 'Pay $9.99';
return;
}
// Send the PaymentMethod ID to your server
const result = await fetch('/create-payment-intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
paymentMethodId: paymentMethod.id,
amount: 999, // $9.99 in cents
}),
});
const paymentData = await result.json();
// Handle the result
const paymentResult = document.getElementById('payment-result');
const paymentResultMessage = document.getElementById('payment-result-message');
paymentResult.classList.remove('hidden');
paymentResultMessage.textContent = JSON.stringify(paymentData, null, 2);
// If the payment requires additional action, handle it
if (paymentData.requiresAction) {
const { error, paymentIntent } = await stripe.handleCardAction(
paymentData.clientSecret
);
if (error) {
paymentResultMessage.textContent = 'Payment failed: ' + error.message;
} else {
// The card action has been handled
// Confirm the payment with the server
const confirmResult = await fetch('/confirm-payment', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
paymentIntentId: paymentIntent.id,
}),
});
const confirmData = await confirmResult.json();
paymentResultMessage.textContent = JSON.stringify(confirmData, null, 2);
}
}
} catch (err) {
console.error('Error:', err);
const errorElement = document.getElementById('card-errors');
errorElement.textContent = 'An unexpected error occurred.';
} finally {
// Re-enable the submit button
submitButton.disabled = false;
submitButton.textContent = 'Pay $9.99';
}
});
});
Step 7: Implement Backend Routes
Add the following routes to your server.js
file:
// Route to expose the publishable key to the frontend
app.get('/config', (req, res) => {
res.json({
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
});
});
// Route to create a PaymentIntent
app.post('/create-payment-intent', async (req, res) => {
try {
const { paymentMethodId, amount } = req.body;
// Create a PaymentIntent with the order amount and currency
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: 'usd',
payment\_method: paymentMethodId,
confirmation\_method: 'manual',
confirm: true,
// Verify your integration by passing this to 'automatic' (default) for your first implementation.
// After that, you can remove this parameter.
setup_future_usage: 'off\_session',
});
// Handle the response based on the PaymentIntent status
if (
paymentIntent.status === 'requires\_action' &&
paymentIntent.next_action.type === 'use_stripe\_sdk'
) {
// Tell the client to handle the action
res.json({
requiresAction: true,
clientSecret: paymentIntent.client\_secret,
});
} else if (paymentIntent.status === 'succeeded') {
// The payment didn't need any additional actions and is completed!
res.json({
success: true,
paymentIntent: {
id: paymentIntent.id,
status: paymentIntent.status,
},
});
} else {
// Invalid status
res.json({
error: 'Invalid PaymentIntent status',
});
}
} catch (err) {
console.error('Error creating payment intent:', err);
res.status(500).json({ error: err.message });
}
});
// Route to confirm a payment after handling additional actions
app.post('/confirm-payment', async (req, res) => {
try {
const { paymentIntentId } = req.body;
const paymentIntent = await stripe.paymentIntents.confirm(paymentIntentId);
if (paymentIntent.status === 'succeeded') {
res.json({
success: true,
paymentIntent: {
id: paymentIntent.id,
status: paymentIntent.status,
},
});
} else {
res.json({
error: 'Payment confirmation failed',
paymentIntent: {
id: paymentIntent.id,
status: paymentIntent.status,
},
});
}
} catch (err) {
console.error('Error confirming payment:', err);
res.status(500).json({ error: err.message });
}
});
Step 8: Run Your Application
Start your Node.js server:
node server.js
Open your browser and navigate to http://localhost:3000
to test your Stripe integration.
For testing, you can use Stripe's test card numbers:
- 4242 4242 4242 4242 - Successful payment
- 4000 0025 0000 3155 - Requires authentication (3D Secure)
- 4000 0000 0000 9995 - Declined payment
Step 9: Implement Webhooks (Advanced)
For production applications, you should implement Stripe Webhooks to reliably handle asynchronous events:
// Add this to your server.js file
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
} catch (err) {
console.error(`Webhook Error: ${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(`PaymentIntent for ${paymentIntent.amount} was successful!`);
// Then define and call a function to handle the event payment\_intent.succeeded
break;
case 'payment_intent.payment_failed':
const failedPaymentIntent = event.data.object;
console.log(`Payment failed: ${failedPaymentIntent.last_payment_error?.message}`);
// Then define and call a function to handle the failed payment
break;
// Handle other event types as needed
default:
console.log(`Unhandled event type ${event.type}`);
}
// Return a 200 response to acknowledge receipt of the event
res.send();
});
Note: To use webhooks locally, you'll need to use the Stripe CLI or a service like ngrok to forward webhook events to your local server.
Step 10: Implement Subscription Billing (Advanced)
To implement subscription billing with Stripe, you can add the following code:
// Create a subscription plan
app.post('/create-subscription', async (req, res) => {
try {
const { paymentMethodId, customerId, priceId } = req.body;
// If no customer ID is provided, create a new customer
let customer;
if (!customerId) {
customer = await stripe.customers.create({
payment\_method: paymentMethodId,
email: req.body.email,
invoice\_settings: {
default_payment_method: paymentMethodId,
},
});
} else {
// Attach the payment method to the existing customer
await stripe.paymentMethods.attach(paymentMethodId, {
customer: customerId,
});
// Set it as the default payment method
await stripe.customers.update(customerId, {
invoice\_settings: {
default_payment_method: paymentMethodId,
},
});
customer = await stripe.customers.retrieve(customerId);
}
// Create the subscription
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [{ price: priceId }],
expand: ['latest_invoice.payment_intent'],
});
res.json({
subscriptionId: subscription.id,
clientSecret: subscription.latest_invoice.payment_intent.client\_secret,
customerId: customer.id,
});
} catch (error) {
console.error('Error creating subscription:', error);
res.status(500).json({ error: error.message });
}
});
// Cancel a subscription
app.post('/cancel-subscription', async (req, res) => {
try {
const { subscriptionId } = req.body;
const subscription = await stripe.subscriptions.update(subscriptionId, {
cancel_at_period\_end: true,
});
res.json({
success: true,
subscription: {
id: subscription.id,
status: subscription.status,
cancel_at_period_end: subscription.cancel_at_period_end,
current_period_end: subscription.current_period_end,
},
});
} catch (error) {
console.error('Error cancelling subscription:', error);
res.status(500).json({ error: error.message });
}
});
Step 11: Implement Customer Management (Advanced)
To manage customers in Stripe, add these routes:
// Create a customer
app.post('/create-customer', async (req, res) => {
try {
const { email, name, paymentMethodId } = req.body;
const customer = await stripe.customers.create({
email,
name,
payment\_method: paymentMethodId,
invoice\_settings: {
default_payment_method: paymentMethodId,
},
});
res.json({
success: true,
customer: {
id: customer.id,
email: customer.email,
name: customer.name,
},
});
} catch (error) {
console.error('Error creating customer:', error);
res.status(500).json({ error: error.message });
}
});
// Get customer details
app.get('/customers/:id', async (req, res) => {
try {
const { id } = req.params;
const customer = await stripe.customers.retrieve(id, {
expand: ['subscriptions', 'payment\_methods'],
});
res.json({
success: true,
customer,
});
} catch (error) {
console.error('Error retrieving customer:', error);
res.status(500).json({ error: error.message });
}
});
// Update customer
app.post('/customers/:id', async (req, res) => {
try {
const { id } = req.params;
const { email, name } = req.body;
const customer = await stripe.customers.update(id, {
email,
name,
});
res.json({
success: true,
customer: {
id: customer.id,
email: customer.email,
name: customer.name,
},
});
} catch (error) {
console.error('Error updating customer:', error);
res.status(500).json({ error: error.message });
}
});
Step 12: Error Handling and Best Practices
Enhance your application with proper error handling:
// Middleware for better error handling
app.use((err, req, res, next) => {
console.error('Application error:', err);
if (err.type === 'StripeCardError') {
// Card errors, such as declined card
return res.status(400).json({
error: {
message: err.message,
code: err.code,
type: err.type,
}
});
} else if (err.type === 'StripeInvalidRequestError') {
// Invalid parameters were supplied to Stripe's API
return res.status(400).json({
error: {
message: 'Invalid parameters provided to the Stripe API',
code: err.code,
type: err.type,
}
});
} else if (err.type === 'StripeAPIError') {
// An error occurred internally with Stripe's API
return res.status(500).json({
error: {
message: 'An error occurred with Stripe',
code: err.code,
type: err.type,
}
});
} else if (err.type === 'StripeConnectionError') {
// Some kind of error occurred during the HTTPS communication
return res.status(503).json({
error: {
message: 'Could not connect to Stripe servers',
code: err.code,
type: err.type,
}
});
} else if (err.type === 'StripeAuthenticationError') {
// You probably used an incorrect API key
return res.status(401).json({
error: {
message: 'Authentication with Stripe failed',
code: err.code,
type: err.type,
}
});
} else {
// Generic error handling
return res.status(500).json({
error: {
message: 'An unexpected error occurred',
}
});
}
});
Conclusion
Congratulations! You've successfully integrated Stripe's payment processing API with your Node.js application. You've learned how to:
- Set up your Stripe account and get API keys
- Create a basic Node.js Express server
- Implement a client-side payment form with Stripe Elements
- Process one-time payments with PaymentIntents
- Implement webhooks for handling asynchronous events
- Set up subscription billing
- Manage customers in Stripe
- Implement proper error handling
This tutorial covers the basics, but Stripe offers many more features such as invoicing, connect platform, tax calculation, and more. Refer to the Stripe API documentation for more advanced features and use cases.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.