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:

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:

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):

  1. GET /v1/payment_intents/{id} — your most-used payment flow
  2. POST /v1/payment_intents/{id}/confirm — the confirmation surface
  3. GET /v1/subscriptions/{id} — if you have subscriptions
  4. GET /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:

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