Handling Stripe API versions: what developers should know
You've built a solid payment flow using Stripe, and everything works smoothly in production. Then Stripe announces a new API version with better webhook filtering, improved error messages, and faster processing. The challenge: how do you upgrade without accidentally breaking payments for your customers? Handling Stripe API versions: what developers should know is critical infrastructure work that most teams delay until it becomes urgent—but a structured approach prevents costly incidents and keeps your payment system resilient.
Unlike REST APIs where all clients move together, Stripe maintains multiple API versions simultaneously. Your account receives a default version, but you can pin specific versions per API call, webhook endpoint, or SDK client. This flexibility is powerful, but it also means you need a clear strategy to avoid version mismatches, test breaking changes safely, and document your upgrade path. Over 58 days of payment engineering, we've seen teams stumble here: production bugs from untested version upgrades, webhook desynchronization between endpoints, and confusion about which breaking changes actually affect their code. Let's walk through the mechanics and best practices.
Pin versions — Set the API version in your SDK client
Every request to Stripe's API uses a specific version. By default, your Stripe account has a default API version set in your dashboard (currently 2023-10-16 or later depending on your account creation date). However, you shouldn't rely solely on account-level defaults. Instead, pin the version explicitly in your SDK client initialization.
When you create your Stripe client, you're setting the version for all API calls made through that client instance. This ensures consistency and makes it obvious to anyone reading your code which version your payment system targets.
// Node.js / TypeScript example
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2023-10-16', // Pin to a known, tested version
});
// All subsequent calls use this version
const paymentIntent = await stripe.paymentIntents.create({
amount: 5000,
currency: 'usd',
payment_method_types: ['card'],
});
By pinning the version in your SDK, you control exactly which API behavior your application receives. When Stripe rolls out a new version—say 2024-04-10 with changes to refund handling—your existing code continues to use 2023-10-16 until you explicitly test and upgrade.
The alternative is to let your account-level default control everything. This creates invisible risk: a Stripe push to a new default version could silently change how your payment system behaves. We've seen teams experience unexpected issues in production because they didn't pin versions and a new account default went live overnight.
Store your pinned version in a configuration file or environment variable, not hard-coded strings scattered across your codebase. This makes it easy to upgrade across your entire application:
// config/stripe.ts
export const STRIPE_API_VERSION = '2023-10-16';
// services/payment.ts
import { STRIPE_API_VERSION } from '../config/stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: STRIPE_API_VERSION,
});
Webhook endpoint versions — Each endpoint has its own version
Webhooks are where many version-related surprises surface. Your Stripe webhook endpoint—the URL that receives events like charge.succeeded or payment_intent.succeeded—runs a specific API version independent of your SDK client version. This means you can have misalignment: your backend SDK uses version 2023-10-16, but your webhooks receive events formatted for version 2024-04-10.
Each webhook endpoint in your Stripe dashboard has its own version setting. Check your dashboard under Developers > Webhooks to see the version assigned to each endpoint. When you update your code to handle a breaking change, you must also update the webhook endpoint version to match.
Here's a concrete scenario: Stripe's 2024-04-10 version changes the structure of the source object in charge events. In the old version, charge.source.type was a simple string. In the new version, it's an object with nested properties. If your webhook handler expects the old structure but receives the new one, your parser may fail silently or throw an unhandled error.
// Webhook handler using old version format
app.post('/webhooks/stripe', (req, res) => {
const event = req.body;
if (event.type === 'charge.succeeded') {
const charge = event.data.object;
// Old version: charge.source.type is "card"
// New version: charge.source.type is { card: { ...details } }
const cardType = charge.source.type; // May break or log undefined
console.log(`Card type: ${cardType}`);
}
res.json({ received: true });
});
To keep webhooks in sync, update your endpoint version through the Stripe dashboard before deploying code changes that depend on the new format. Or deploy your code first (since you're handling both formats), then update the endpoint version. The key is to never let them drift unintentionally.
// Safer webhook handler that handles both versions
app.post('/webhooks/stripe', (req, res) => {
const event = req.body;
if (event.type === 'charge.succeeded') {
const charge = event.data.object;
const source = charge.source;
// Handle both old and new formats
const cardType = typeof source.type === 'string'
? source.type
: source.type?.card ? 'card' : 'unknown';
console.log(`Card type: ${cardType}`);
}
res.json({ received: true });
});
Once you've verified your code handles the new format correctly, update the webhook endpoint version in the dashboard. This two-phase approach minimizes the window of potential mismatches.
Upgrade in steps — Test new versions in a sandbox
Never upgrade Stripe API versions directly in production. The process should be: development environment, staging environment, then production. In your development and staging setups, you use Stripe's test mode with test API keys, so you can safely experiment.
Start by creating a test Stripe client that uses the new API version. Run your full payment flow against this client: create payment intents, handle webhooks, process refunds, dispute a charge. Every payment operation your system supports should be tested with the new version.
// Test script: compare old vs new API version
import Stripe from 'stripe';
const CURRENT_VERSION = '2023-10-16';
const NEW_VERSION = '2024-04-10';
const stripeOld = new Stripe(process.env.STRIPE_TEST_KEY, {
apiVersion: CURRENT_VERSION,
});
const stripeNew = new Stripe(process.env.STRIPE_TEST_KEY, {
apiVersion: NEW_VERSION,
});
async function testPaymentFlow() {
console.log('Testing with current version:', CURRENT_VERSION);
const intentOld = await stripeOld.paymentIntents.create({
amount: 2000,
currency: 'usd',
payment_method_types: ['card'],
});
console.log('Old response:', JSON.stringify(intentOld, null, 2));
console.log('\\nTesting with new version:', NEW_VERSION);
const intentNew = await stripeNew.paymentIntents.create({
amount: 2000,
currency: 'usd',
payment_method_types: ['card'],
});
console.log('New response:', JSON.stringify(intentNew, null, 2));
// Compare structures to identify breaking changes
}
testPaymentFlow().catch(console.error);
In staging, run your full integration test suite with the new version. Simulate high-traffic scenarios. Process test payments, create subscriptions, handle webhook retries, test error paths. If any test fails, dig into the changelog to understand the breaking change and adjust your code accordingly.
Only after staging passes successfully should you upgrade production. Even then, consider doing a phased rollout if you control it: upgrade 10% of traffic first, monitor for errors, then gradually increase. With Stripe SDKs, you might achieve this by running multiple Stripe client instances with different versions and routing a percentage of requests to each, but for most teams, a full cutover is acceptable once staging is clean.
Read changelogs — Breaking changes are documented per version
Stripe publishes a detailed changelog for every API version. Before upgrading, read it. Breaking changes are clearly labeled, and Stripe explains what changed and why. This saves time diagnosing issues later.
Visit https://stripe.com/docs/upgrades and navigate to the specific version you're considering. The changelog includes:
- Removed fields — Fields that no longer exist in the response. Your code accessing these fields will receive undefined or null.
- Renamed fields — A field like
sourcebecomespayment_method. You must update references. - Changed behavior — A method now requires an additional parameter, or error handling has changed.
- Deprecated features — Features that still work but will be removed in a future version. Plan to migrate away.
Create a checklist from the changelog. Go through your codebase—payment creation, webhook handling, subscription management, refunds, disputes—and identify which breaking changes affect you. Update your code, test it, then upgrade the version. For example, if the changelog says "charge.outcome.type is now a structured object instead of a string," search your codebase for references to outcome.type and adapt them.
Stripe's changelog also mentions new features and deprecations. New features are optional; you can adopt them on your own timeline. Deprecations give you advance notice before something is removed entirely, typically several major versions in advance.
Common mistakes
- Relying on account-level defaults — Don't assume your Stripe account's default version matches what your code expects. Pin it explicitly in your SDK and document your version choice.
- Forgetting to update webhook endpoint versions — You upgrade your SDK client, but your webhook endpoint still uses the old version. Events arrive in an unexpected format, and your handler silently drops data. Always sync SDK and webhook versions.
- Upgrading directly to production — Skipping the staging test. A breaking change you didn't catch in the changelog causes a payment creation to fail in production. Always test in a non-production environment first.
- Not reading the changelog — Guessing which changes affect your code is slow and error-prone. Stripe's changelog is detailed and searchable. Use it.
- Leaving old version references scattered throughout the codebase — One team member upgrades the SDK version but forgets to update your configuration file. You end up with version mismatches. Centralize version management.
- Testing only the happy path — You test successful payments with the new version but don't test failed payments, refunds, or dispute workflows. Failures often expose version-specific issues.
Key takeaways
- Pin your Stripe SDK client to a specific API version using the
apiVersionparameter. This prevents silent changes when Stripe updates your account's default. - Webhook endpoints have their own independent version setting. Update them alongside your SDK to keep event formats in sync with your handler code.
- Test new API versions in development and staging environments before production. Run your full payment flow: intent creation, confirmations, webhooks, refunds, subscriptions.
- Always read the Stripe changelog for the target version. Build a checklist of breaking changes and verify your code handles each one.
- Deploy code changes first (handling both old and new formats if needed), then update the endpoint or SDK version. This minimizes the window of misalignment.
- Consider a phased or staged rollout for production upgrades if your infrastructure supports it, or upgrade all at once after thorough staging tests.
- Document your pinned API version in comments or configuration files so team members understand why a specific version was chosen.






Comments
Loading comments…