How We Caught a Breaking Stripe API Change Before It Hit Production
It was a Tuesday morning. No incidents in the queue, no deploys scheduled. Then Rumbliq fired an alert.
⚠️ Schema drift detected: Stripe Payment Intents API
Monitor: stripe-payment-intents-confirm
Endpoint: https://api.stripe.com/v1/payment_intents/{id}/confirm
Detected at: 2026-02-18 09:14:23 UTC
Changes: 3 field changes
Severity: High
The team opened the diff. What they saw explained exactly why Stripe had been quiet about it: the change looked minor on paper but was a silent breaking change for any code doing strict field validation.
This is the story of that detection — and what would have happened without it.
The Setup: Monitoring Stripe with Rumbliq
Before diving into the incident, here's how the monitoring was configured.
The team used Rumbliq to monitor eight Stripe API endpoints that their payment processing code depended on:
GET /v1/payment_intents/{id}— retrieve payment intentPOST /v1/payment_intents/{id}/confirm— confirm payment intentGET /v1/customers/{id}— retrieve customerGET /v1/subscriptions/{id}— retrieve subscriptionGET /v1/invoices/{id}— retrieve invoicePOST /v1/charges— create chargeGET /v1/refunds/{id}— retrieve refundGET /v1/payment_methods/{id}— retrieve payment method
Each monitor calls the endpoint with a test API key against Stripe's test mode, captures the JSON response, extracts the full response schema, and diffs it against the stored baseline.
The alert configuration:
{
"monitor": "stripe-payment-intents-confirm",
"endpoint": "https://api.stripe.com/v1/payment_intents/pi_test_xxx/confirm",
"method": "POST",
"headers": {
"Authorization": "Bearer sk_test_..."
},
"interval": "1m",
"alerts": [
{
"type": "slack",
"webhook": "https://hooks.slack.com/...",
"channel": "#payments-eng"
},
{
"type": "email",
"address": "[email protected]"
}
]
}
Polling every minute on Stripe's revenue-critical endpoints. The whole setup took about 20 minutes.
The Alert: What the Diff Showed
When the alert fired, here's what Rumbliq detected in the Payment Intents confirm response:
Before (baseline schema):
{
"id": "string",
"object": "string",
"amount": "number",
"currency": "string",
"status": "string",
"payment_method_options": {
"card": {
"installments": "null | object",
"mandate_options": "null | object",
"network": "null | string",
"request_three_d_secure": "string"
}
},
"next_action": "null | object",
"charges": {
"object": "string",
"data": "array",
"has_more": "boolean",
"url": "string"
}
}
After (new schema):
{
"id": "string",
"object": "string",
"amount": "number",
"currency": "string",
"status": "string",
"payment_method_options": {
"card": {
"installments": "null | object",
"mandate_options": "null | object",
"network": "null | string",
"request_three_d_secure": "string",
"capture_method": "string"
}
},
"next_action": "null | object",
"latest_charge": "null | string"
}
Detected changes:
| Field | Change Type | Impact |
|---|---|---|
payment_method_options.card.capture_method |
Added field | Additive — safe |
charges |
Removed field | Breaking — any code reading charges.data will fail |
latest_charge |
Added field (replaces charges) |
Additive — safe, but replaces removed field |
The charges object had been replaced with a latest_charge string field. Stripe had migrated from returning the charges list inline to returning just the latest charge ID, with the full list available via a separate API call.
This was documented in Stripe's API changelog — but the team hadn't seen it, and none of their internal tests covered this specific response path.
What the Code Was Doing
The payment confirmation service had this processing logic:
async function processPaymentConfirmation(paymentIntentId: string) {
const intent = await stripe.paymentIntents.confirm(paymentIntentId, {
payment_method: 'pm_card_visa',
});
// Extract charge ID for receipt generation
const chargeId = intent.charges?.data?.[0]?.id;
if (!chargeId) {
logger.warn('No charge found after confirmation', { paymentIntentId });
// Falls through — receipt generation skipped
}
await generateReceipt({
paymentIntentId,
chargeId,
amount: intent.amount,
currency: intent.currency,
});
}
With the schema change, intent.charges would be undefined. The chargeId extraction would return undefined. The generateReceipt call would be made with chargeId: undefined.
The receipt generation service accepted undefined chargeId — it would just generate a receipt with a blank charge reference. No error thrown. No alert fired by existing monitoring.
Customers would receive receipts with a missing transaction reference. Accounting reconciliation would break. Some payment processors use the charge ID for refund requests — those flows would fail silently.
All without any error appearing in the application logs.
The Fix: 2 Hours After Detection
Once the alert fired and the team understood the change, the fix was straightforward:
async function processPaymentConfirmation(paymentIntentId: string) {
const intent = await stripe.paymentIntents.confirm(paymentIntentId, {
payment_method: 'pm_card_visa',
});
// Stripe migrated from charges.data[0].id to latest_charge
// See: Stripe API changelog, 2026-02-18
const chargeId = intent.latest_charge ?? intent.charges?.data?.[0]?.id;
if (!chargeId) {
logger.warn('No charge found after confirmation', { paymentIntentId });
throw new Error('Payment confirmed but no charge reference found');
}
await generateReceipt({
paymentIntentId,
chargeId,
amount: intent.amount,
currency: intent.currency,
});
}
The fix was deployed the same morning, before any production transactions were processed with the new Stripe response format.
After deploying, the team reset the Rumbliq baseline to accept the new schema. Future monitoring would track drift against the updated expected structure.
The Timeline
09:14 — Rumbliq detects schema drift, fires Slack alert
09:17 — On-call dev opens the alert
09:22 — Diff reviewed, change identified as breaking
09:35 — Root cause confirmed: Stripe API changelog for 2026-02-18
09:55 — Fix written and in code review
11:40 — Fix deployed to production
11:42 — Baseline updated in Rumbliq
Two hours and twenty-eight minutes from detection to resolution, deployed before any customer transactions were affected.
What Would Have Happened Without Monitoring
The alternative scenario:
- Schema change hits production
- Receipt generation silently produces receipts with missing charge IDs
- Accounting notices reconciliation failures — probably 2-5 days later
- Support tickets start arriving: "My receipt doesn't have a transaction reference"
- Engineering investigates: 4-8 hours of debugging time to find the cause
- Fix deployed, but now you have a backlog of bad receipts to regenerate
- Data remediation work: 1-2 days
- Customer communication if reconciliation affected billing
Conservative estimate: 3-5 days of engineering time, potential customer impact, accounting cleanup. The same math the Stripe on-call team has seen a dozen times.
Setting Up Stripe Monitoring with Rumbliq
If you're running Stripe payments and don't have schema monitoring yet, here's the minimal setup that would catch this class of change:
Start with these four endpoints (the highest-risk Stripe surfaces):
GET /v1/payment_intents/{id}— your most-used payment flowPOST /v1/payment_intents/{id}/confirm— the confirmation surfaceGET /v1/subscriptions/{id}— if you have subscriptionsGET /v1/invoices/{id}— if invoices are part of your flow
Use test mode credentials for monitoring. Stripe's test and live APIs share the same schema — drift shows up in test mode first.
Set 1-minute polling for payment-critical endpoints. Stripe changes propagate gradually — you want to catch them at the leading edge.
Route alerts to whoever owns your Stripe integration — payments team, not DevOps generalists who won't immediately know what charges.data[0].id is or why it matters.
The Broader Pattern
This wasn't a Stripe-specific risk. The same pattern applies to every third-party API your production code depends on:
- Twilio changes how webhook payloads are structured
- OpenAI adds required parameters to completion requests
- GitHub changes the field names in webhook payloads
- AWS SDK updates silently alter response shapes
Every one of these follows the same pattern: the provider makes a change that looks minor from their perspective, your code has undocumented assumptions about the response shape, and the failure is silent until someone reports a symptom.
Schema drift monitoring closes that gap. You monitor the contract your code actually uses, not the contract the provider documents. When they diverge, you know immediately — not after users notice.
The free plan covers 25 monitors — more than enough to cover your critical third-party integrations. Start monitoring free →
Related: Monitoring Stripe API Changes · Detect Breaking API Changes Automatically · API Schema Drift vs Breaking Changes