Web Development · Payments
Stripe in 2026: The Payment APIs Every Web Agency Should Know
Stripe's API surface has grown well beyond checkout forms. Here's the realistic breakdown of which Stripe products matter for agency work, which are overkill, and where teams consistently get burned.
Anurag Verma
8 min read
Sponsored
Most agencies encounter Stripe for the first time through a “take payment” requirement and reach for Stripe Checkout, the hosted payment page that takes 20 minutes to integrate. That’s the right move for a quick prototype. But the moment a client asks for subscriptions, marketplace splits, or tax calculation, you’re either learning the deeper API or building something fragile on top of Checkout.
Stripe’s documentation is excellent but wide. There are products most agencies will never need (Stripe Issuing, Terminal) sitting next to products that should be in every project (Radar, Billing). Here’s the map.
The Four Integration Patterns
Before touching any Stripe product, you need to know which integration pattern fits the project. Choosing wrong creates expensive rework.
Stripe Checkout: Hosted page, Stripe handles the UI. Redirect users to checkout.stripe.com, they pay, Stripe redirects back. Works for one-time payments and subscriptions. Takes 30 minutes to ship. Not customizable beyond basic color settings and logo.
Stripe Payment Element: Embedded iframe, Stripe handles PCI. Drop the Payment Element into your page with Stripe.js. You control the surrounding UI. Customers never leave your site. Supports 30+ payment methods (cards, Apple Pay, Link, SEPA, etc.) with a single element.
Stripe Elements: Individual components (card number, expiry, CVC as separate fields). Use when you need a specific layout that doesn’t work with the single Payment Element block.
Direct API: Raw API calls for server-side flows, MOTO (mail order/telephone order), or when you’ve achieved PCI SAQ-D compliance. Most agencies should never reach for this.
For a typical SaaS product: start with Payment Element. It’s the right default for 2026.
Stripe Billing for Subscriptions
If a project has recurring revenue, Stripe Billing is the correct place to build it. Not a custom subscriptions table in your database.
The core objects:
- Product — what you’re selling (“Pro Plan”)
- Price — how you’re billing for it ($99/month, $999/year, usage-based)
- Customer — the billing entity
- Subscription — links a customer to a price, manages renewal, proration, and status
- Invoice — generated automatically at each billing cycle
// Create a subscription via the API (server-side)
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
// 1. Create or retrieve a customer
const customer = await stripe.customers.create({
email: user.email,
metadata: { user_id: user.id },
})
// 2. Attach a payment method to the customer
await stripe.paymentMethods.attach(paymentMethodId, {
customer: customer.id,
})
// Set it as the default
await stripe.customers.update(customer.id, {
invoice_settings: { default_payment_method: paymentMethodId },
})
// 3. Create the subscription
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [{ price: 'price_1234yourPriceId' }],
payment_behavior: 'default_incomplete',
expand: ['latest_invoice.payment_intent'],
})
The payment_behavior: 'default_incomplete' pattern is important. Instead of trying to charge immediately (which fails silently if the card requires 3DS authentication), Stripe creates the subscription in an incomplete state and you confirm the payment on the client side. This handles Strong Customer Authentication (SCA) for European cards correctly.
Stripe Customer Portal is the most underused feature in agency work. It’s a hosted portal that lets customers manage their own subscriptions: update cards, switch plans, cancel. You can brand it with your logo. Most teams build their own subscription management UI when they could have this for free in an afternoon.
// Create a Customer Portal session
const portalSession = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: 'https://yourdomain.com/account',
})
// Redirect the user to portalSession.url
Webhooks: The Part That Actually Takes Time
Stripe is asynchronous. A payment succeeds, fails, or requires action, and you find out via webhook. This is where most integrations have bugs.
The events you need to handle for a subscription product:
// Webhook handler — Express example
import { buffer } from 'micro' // or equivalent body parsing
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!
app.post('/webhooks/stripe', async (req, res) => {
const payload = req.rawBody // must be raw, not parsed JSON
const sig = req.headers['stripe-signature']
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(payload, sig, webhookSecret)
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`)
}
switch (event.type) {
case 'checkout.session.completed':
// Payment succeeded via Checkout — provision access
break
case 'customer.subscription.created':
// New subscription — update your DB
break
case 'customer.subscription.updated':
// Plan changed, trial ended, etc.
break
case 'customer.subscription.deleted':
// Subscription canceled or lapsed — revoke access
break
case 'invoice.payment_failed':
// Retry logic, dunning emails
break
case 'invoice.paid':
// Successful renewal — extend access period in DB
break
}
res.json({ received: true })
})
Critical: always use stripe.webhooks.constructEvent to verify the webhook signature. Never process a webhook payload you didn’t verify.
Use the Stripe CLI for local development:
stripe listen --forward-to localhost:3000/webhooks/stripe
This gives you a local webhook secret that works for development.
Stripe Tax: Worth It on International Products
Stripe Tax calculates and collects sales tax, VAT, and GST automatically based on the customer’s location. You don’t maintain tax tables or write jurisdiction logic. It costs 0.5% of transactions with tax applied ($2 minimum per month).
Enable it on a price by adding automatic_tax:
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
customer_email: user.email,
automatic_tax: { enabled: true },
line_items: [{
price: 'price_yourid',
quantity: 1,
}],
success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${baseUrl}/pricing`,
})
For products sold only in one country with simple tax requirements, it’s probably not worth 0.5% of revenue. For products with EU customers (where VAT OSS applies) or with US customers across multiple states (where sales tax thresholds vary), it pays for itself in engineering time saved.
Stripe Connect: Platform and Marketplace Payments
If your client is building a marketplace (money flows from buyers to sellers with a platform fee taken in between), you need Stripe Connect. This is a significant integration, not a weekend project.
There are two patterns:
Standard accounts: Sellers create their own Stripe accounts and connect them to your platform. Stripe handles KYC, payouts, and disputes with the seller directly. Lowest engineering overhead but least control.
Express accounts: Sellers onboard through a Stripe-hosted flow but your platform manages payout timing and fee structure. Stripe still handles KYC. Most marketplace products use this.
// Initiate Express onboarding for a seller
const accountLink = await stripe.accountLinks.create({
account: sellerStripeAccountId,
refresh_url: 'https://yourdomain.com/connect/refresh',
return_url: 'https://yourdomain.com/connect/return',
type: 'account_onboarding',
})
// redirect seller to accountLink.url
// Create a charge with a platform fee
const paymentIntent = await stripe.paymentIntents.create({
amount: 5000, // $50.00 in cents
currency: 'usd',
application_fee_amount: 500, // $5.00 platform fee
transfer_data: {
destination: sellerStripeAccountId,
},
})
Budget 3-5 weeks of engineering for a Connect integration. The onboarding, KYC edge cases, and payout reporting are more work than the actual payment flow.
Stripe Radar: Turn It On, Configure It
Stripe Radar is included in every Stripe account and does machine learning-based fraud detection. By default, it blocks transactions it flags as high-risk. You can also add custom rules.
A few rules worth adding in Radar’s dashboard:
- Block cards from specific high-fraud countries if you don’t ship there
- Block transactions where billing address ZIP doesn’t match (AVS mismatch) on amounts above a threshold
- Review orders where a new account pays for a high-value item in the first hour
Radar has a review queue where flagged transactions sit until you manually allow or block them. For products with high average order values, checking the review queue daily is worth building into your process.
What to Ignore
Stripe Terminal: For in-person card readers. Only relevant if your client has a physical location or events. Skip unless explicitly required.
Stripe Issuing: For creating corporate cards. Specialized product for fintech apps. Not an agency staple.
Stripe Climate: Donate a percentage of revenue to carbon removal. Opt-in, noble cause, not a required integration.
Stripe Sigma: SQL analytics over your Stripe data. Useful for business intelligence but redundant if you’re already pushing Stripe events to a data warehouse via webhooks.
The Part That Always Catches Teams Off-Guard
Idempotency keys. Stripe’s API is not idempotent by default. If your server sends a create charge request and the network drops before you get a response, re-sending the same request creates a duplicate charge.
// Always use idempotency keys for create operations
const paymentIntent = await stripe.paymentIntents.create(
{
amount: 2000,
currency: 'usd',
customer: customerId,
},
{
idempotencyKey: `pi_create_${orderId}_${userId}`,
}
)
Use a deterministic key derived from your operation and entity IDs. If you retry, Stripe returns the same PaymentIntent instead of creating a second one. This is not optional for production systems.
Stripe’s API is genuinely one of the better ones in the industry, but the surface area has grown to the point where you need a map. Checkout and Payment Element handle most agency work. Add Billing for subscriptions, Tax for international products, and Connect only when the business model actually requires it.
Sponsored
More from this category
More from Web Development
Sponsored
The dispatch
Working notes from
the studio.
A short letter twice a month — what we shipped, what broke, and the AI tools earning their keep.
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored