← All provider guides

Stripe payment_records migration: four silent breaking changes in one pattern

stripe 4 adjudicated cases measured on 47 spec pairs

When a provider migrates a core resource, the individual spec diffs rarely look dramatic. Stripe's ongoing payment_records migration is a case study: across four separate API releases, fields inside payment_method_details were quietly made nullable, made optional, or removed outright. Each change is real reader-breaking behavior — code that reads those fields breaks — and each one is invisible to a default severity-gated diff tool. This page documents all four cases with independently adjudicated evidence from a 47-pair Stripe spec corridor.

The pattern

Stripe is consolidating payment detail schemas under the payment_records resource. During the migration, $ref anchors inside payment_method_details are re-pointed from legacy schemas to new payment_record variants — and the new variants relax the contract: properties that were required and non-nullable become optional and nullable, or disappear. The OpenAPI diff for each release is one small schema change. The blast radius is every consumer that reads those fields.

Case 1 — boleto tax_id (v2182 → v2183)

A single $ref re-anchor pointed payment_method_details.boleto at a new payment_record schema where tax_id is optional and nullable. One root cause — but a naive transitive diff fans it out to 1,162 findings across hundreds of embedding schemas. mendapi's specdiff reports the 12 canonical anchors and deliberately stops at transitive embeds: one change, one page of alerts, not 1,162. That stop rule is a design choice against alert fatigue, not a detection difference — both engines saw the change.

Case 2 — alma installments.count (v2248 → v2249)

Same re-anchor pattern: alma.installments.count went from required and non-nullable to nullable with no required list. specdiff completed in 15.4s and reported 9 real breaking units, each verified by walking both raw specs. The comparison engine we benchmark against did not complete at all on this pair: three independent runs hit a 900-second hard timeout with zero bytes of output. If your gate never finishes, its verdict for that release is silence.

Case 3 — card core fields relaxed (v2200 → v2210)

The largest batch: brand, exp_month, exp_year, funding and last4 on payment_records card details — the five fields nearly every reconciliation or receipt pipeline reads — all went optional and nullable, part of 96 adjudicated-real breaking units in this release (zero false positives across five root-cause families). This pair was also a timeout on the comparison engine: two independent 900-second runs, 32 CPU-minutes, no output. specdiff returned in 14.6s.

Case 4 — card intelligence fields removed (v2323 → v2324)

Four card-intelligence properties — description, iin, issuer, stored_credential_usage — were removed from payment_records card details. Risk and reconciliation readers that depend on iin or issuer break outright. The comparison engine detected the same removals but classified all of them as level-2 warnings among 587,122 total findings, so a default level≥3 breaking gate reports zero. To be precise: it did not miss the change — it filed it where a default gate does not look.

The diff itself, byte for byte

Case 4 is the one with a shipping codemod. This is the actual changes.patch the stripe-payment-record-card-details-removal pack's dry run produces on the bundled fixture — embedded here from the golden regression evidence at page build time, not retyped. Reads of the four withdrawn fields (issuer, description, iin, stored_credential_usage) are removed, and the destructuring pattern that pulls issuer alongside brand is narrowed to the surviving field instead of deleted. Note what the codemod leaves alone: reads of fields that survive the change — brand, network — are context, not rewrite, and the comment explaining that the live brand binding stays intact survives byte-for-byte. The rule is anchored to the withdrawn payment-record card surface, not to any identifier that happens to say card.

--- a/index.js
+++ b/index.js
@@ -9,9 +9,6 @@
   const rec = await stripe.paymentRecords.retrieve(id);
   const summary = {
     brand: rec.payment_method_details.card.brand,
-    bank: rec.payment_method_details.card.issuer,
-    label: rec.payment_method_details.card.description,
-    binPrefix: rec.payment_method_details.card.iin,
   };
   const recurring = rec.payment_method_details.card.stored_credential_usage === 'recurring';
   if (recurring) summary.recurring = true;
@@ -23,13 +20,11 @@
   return page.data.map((r) => ({
     amount: r.amount,
     network: r.payment_method_details.card.network,
-    firstSix: r.payment_method_details.card.iin,
   }));
 }
 
 async function auditUsage(id) {
   const rec = await stripe.paymentRecords.retrieve(id);
-  console.log(rec.payment_method_details.card.stored_credential_usage);
   return rec.amount;
 }
 
@@ -38,7 +33,7 @@
 // live brand binding and its reference intact.
 async function brandOnly(id) {
   const rec = await stripe.paymentRecords.retrieve(id);
-  const { issuer, brand } = rec.payment_method_details.card;
+  const { brand } = rec.payment_method_details.card;
   return brand;
 }

Why default gates stay silent

Two failure modes, both measured on this corridor of 47 real Stripe spec pairs:

  • Severity classification. Optional-property removal and nullability relaxation default to warning level in mature diff tools. That is a defensible general policy — and exactly wrong for reader code, which breaks on absent fields regardless of the optional flag. 58 of the 163 default-gate-invisible units on this corridor are of this kind.
  • Availability. On 3 of 47 pairs the comparison engine hit a 15-minute timeout with no output; 105 adjudicated-real breaking units sat inside two of those releases. A gate that times out fails open.

Full methodology, per-pair reports and the honest-citation rules (including where the comparison engine is simply the better tool) are on the when-to-use page.

Am I affected?

If your code reads payment_method_details from payment_records — reconciliation, receipts, risk scoring, exports — check locally in about 30 seconds, with no code leaving your machine:

npx mendapi sync
# -> one network call: pulls the change feed into a local database
npx mendapi scan
# -> findings: file, line, provider, confidence
mendapi deps --match
# -> which of these four changes actually hit your endpoints

Related

Every entry on this page is generated from the mendapi change database at build time and links its upstream source.