Introduction
Integrating Stripe is a critical step for any system requiring online payments and automated billing. Stripe provides a comprehensive RESTful API, real-time webhooks, and a robust dashboard to manage the entire transaction lifecycle.
This guide covers connecting Stripe to an existing system, handling payments, configuring webhooks, and automating billing — from initial authentication to common pitfalls to avoid.
Prerequisites
Before starting, several elements must be in place:
- A Stripe account (test mode enabled by default)
- An API key available in the Dashboard (Dashboard → Developers → API keys)
- A backend server capable of making HTTPS requests to the Stripe API
- A valid SSL/TLS certificate (Stripe rejects non-secure connections)
Stripe provides two key pairs:
| Type | Usage |
|---|---|
sk_test_… |
Test secret key (never on client side) |
pk_test_… |
Test publishable key (frontend side) |
sk_live_… |
Live secret key |
pk_live_… |
Live publishable key |
Secret keys must never be exposed in frontend code. They should only transit through the backend server.
Authentication and Initial Setup
Stripe authentication relies on Bearer Token. Each API request includes the secret key in the Authorization header.
curl https://api.stripe.com/v1/charges \
-u sk_test_YOUR_KEY: \
-d amount=2000 \
-d currency=eur \
-d description="Order payment #1234" \
-d source=tok_visa
In Node.js, the official SDK simplifies initialization:
const Stripe = require('stripe');
const stripe = Stripe('sk_test_YOUR_KEY');
// Verify connection
const balance = await stripe.balance.retrieve();
console.log(balance.available);
In Python:
import stripe
stripe.api_key = "sk_test_YOUR_KEY"
balance = stripe.Balance.retrieve()
print(balance['available'])
Creating a Payment with Stripe API
Payment Intents: The Recommended Approach
Stripe recommends using Payment Intents for all new projects. This flow natively handles Strong Customer Authentication (SCA), deferred payments, and error management.
The flow involves three steps:
1. Server-side Payment Intent creation:
const paymentIntent = await stripe.paymentIntents.create({
amount: 5000, // in cents
currency: 'eur',
metadata: {
order_id: 'CMD-2026-0042',
customer_reference: 'REF-ABC'
}
});
2. Client-side confirmation with Stripe.js:
const { error } = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: cardElement,
billing_details: {
name: 'John Doe'
}
}
});
3. Server-side verification:
const intent = await stripe.paymentIntents.retrieve(paymentIntent.id);
if (intent.status === 'succeeded') {
// Update order in database
await updateOrderStatus(orderId, 'paid');
}
Handling Payment Statuses
A Payment Intent transitions through several states:
| Status | Meaning |
|---|---|
requires_payment_method |
No payment method provided |
requires_confirmation |
Ready to be confirmed |
requires_action |
Additional authentication required (SCA) |
processing |
Payment being processed |
succeeded |
Payment validated |
requires_capture |
Authorized but not captured |
canceled |
Payment canceled |
Each status transition triggers a webhook event — a crucial point for maintaining synchronization between Stripe and the internal system.
Configuring Stripe Webhooks
Webhooks are the central mechanism for receiving Stripe events in real time. Without them, the system must periodically poll the API, which is unreliable and resource-intensive.
Registering a Webhook Endpoint
From the Stripe Dashboard (Developers → Webhooks) or via the API:
const webhook = await stripe.webhookEndpoints.create({
url: 'https://your-domain.com/api/stripe/webhook',
enabled_events: [
'payment_intent.succeeded',
'payment_intent.payment_failed',
'invoice.paid',
'invoice.payment_failed',
'customer.subscription.created',
'customer.subscription.deleted'
]
});
Webhook Signature Verification
Each event sent by Stripe includes a signature in the Stripe-Signature header. Verification prevents spoofing attacks:
const sig = request.headers['stripe-signature'];
const endpointSecret = 'whsec_YOUR_SECRET';
let event;
try {
event = stripe.webhooks.constructEvent(
request.rawBody,
sig,
endpointSecret
);
} catch (err) {
response.status(400).send(`Webhook Error: ${err.message}`);
return;
}
// Process event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
await fulfillOrder(paymentIntent);
break;
case 'invoice.paid':
const invoice = event.data.object;
await generateInvoicePDF(invoice);
break;
}
Essential Events to Listen To
For a complete billing system, these events are essential:
payment_intent.succeeded— Confirm payment and trigger fulfillmentpayment_intent.payment_failed— Notify and offer alternative payment methodinvoice.paid— Mark invoice as paid, archiveinvoice.payment_failed— Attempt automatic retrycustomer.subscription.updated— Sync subscription plancustomer.subscription.deleted— Revoke access
Connecting Stripe to a Billing System
Creating Scheduled Invoices
Stripe Billing fully automates the recurring billing cycle. Configuration involves creating products and prices:
// Create a product
const product = await stripe.products.create({
name: 'Professional Plan',
description: 'Full platform access'
});
// Create a recurring price
const price = await stripe.prices.create({
product: product.id,
unit_amount: 4900,
currency: 'eur',
recurring: {
interval: 'month'
}
});
Customer Subscriptions
A Stripe customer groups reusable payment information:
const customer = await stripe.customers.create({
email: 'client@example.com',
name: 'Company ABC',
metadata: {
internal_id: 'CLI-0042'
}
});
// Create subscription
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [{ price: price.id }],
payment_behavior: 'default_incomplete',
expand: ['latest_invoice.payment_intent']
});
Invoice Management
Each subscription automatically generates invoices. Retrieval is done via the API:
const invoices = await stripe.invoices.list({
customer: customer.id,
limit: 12,
status: 'paid'
});
// Generate invoice PDF
const invoicePdf = await stripe.invoices.retrievePDF(invoice.id);
Stripe also supports one-off invoicing:
const invoiceItem = await stripe.invoiceItems.create({
customer: customer.id,
amount: 15000,
currency: 'eur',
description: 'Consulting services - March 2026'
});
const invoice = await stripe.invoices.create({
customer: customer.id,
auto_advance: true // automatically generates and sends
});
await stripe.invoices.finalizeInvoice(invoice.id);
await stripe.invoices.sendInvoice(invoice.id);
Automation and Best Practices
Idempotency Keys
Every Stripe API mutation should include an idempotency key. This key prevents double execution in case of timeout or network retry:
const paymentIntent = await stripe.paymentIntents.create({
amount: 5000,
currency: 'eur',
}, {
idempotencyKey: `order-${orderId}-${Date.now()}`
});
Retry Logic with Exponential Backoff
API calls may fail temporarily (rate limit, timeout). A retry strategy with exponential backoff is recommended:
async function stripeCall(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (err) {
if (err.statusCode === 429 || err.statusCode >= 500) {
const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
await new Promise(r => setTimeout(r, delay));
continue;
}
throw err;
}
}
throw new Error('Max retries exceeded');
}
Structured Logging
Every Stripe interaction should be logged with relevant identifiers:
logger.info('Payment intent created', {
payment_intent_id: paymentIntent.id,
amount: paymentIntent.amount,
currency: paymentIntent.currency,
order_id: orderId,
timestamp: new Date().toISOString()
});
Handling Payment Failures
For subscriptions, Stripe offers a configurable automatic retry mechanism (Smart Retries):
await stripe.subscriptions.update(subscription.id, {
days_until_due: 14,
collection_method: 'send_invoice',
payment_settings: {
payment_method_types: ['card', 'sepa_debit']
}
});
An invoice.payment_failed webhook triggers business logic: email notification, temporary access suspension, manual retry.
Testing and Sandbox Mode
Stripe provides a set of test card numbers:
| Card | Result |
|---|---|
4242 4242 4242 4242 |
Successful payment |
4000 0000 0000 0002 |
Declined payment |
4000 0025 0000 3155 |
3D Secure authentication required |
4000 0000 0000 9995 |
Lost card |
The Stripe CLI enables local webhook simulation:
stripe listen --forward-to localhost:3000/api/stripe/webhook
stripe trigger payment_intent.succeeded
Migration from Test to Production
Going live requires several checks:
- Replace test keys with live keys (
sk_live_…/pk_live_…) - Activate the production webhook endpoint with a valid SSL certificate
- Configure enabled payment methods for each currency
- Enable Radar (anti-fraud) and define business rules
- Configure email notifications (receipts, dunning)
- Verify compliance (GDPR, PCI-DSS)
Stripe handles most of PCI-DSS compliance (level 1), but managing sensitive data server-side remains the integrator's responsibility.
FAQ
Which programming languages are supported by Stripe?
Stripe provides official SDKs for Ruby, Python, PHP, Java, Node.js, Go, .NET, and Java. A REST API is available for any other language. The documentation includes examples for each SDK.
How do I handle refunds via the API?
Refunds are processed through the dedicated endpoint:
const refund = await stripe.refunds.create({
payment_intent: 'pi_3AbcDefGhiJkl',
amount: 2500 // partial refund (in cents)
});
A full refund is done by omitting the amount parameter. The charge.refunded webhook notifies the system.
Does Stripe support SEPA payments?
Yes. Stripe supports SEPA Direct Debit for euro direct debits in the 36 SEPA zone countries. Configuration requires activation in the Dashboard and using the sepa_debit payment type. The confirmation cycle is longer than for cards (2-4 business days).
What to do when a webhook is missed?
Stripe provides automatic retry for 72 hours with exponential backoff. On definitive failure, the event appears in the "Failed webhooks" Dashboard section. The API allows relisting recent events:
const events = await stripe.events.list({
type: 'payment_intent.succeeded',
created: { gte: Math.floor(Date.now() / 1000) - 86400 }
});
How do I secure the Stripe integration?
Best practices include: server-side validation of all amounts, systematic use of idempotency keys, webhook signature verification, enabling Radar for fraud detection, restricting payment methods to supported currencies only, and regular audit of Stripe logs.