Stripe “API Version” mismatch causing Metadata Loss

A Stripe API version mismatch occurs when different components of your integration communicate using conflicting Stripe API schemas, causing custom metadata fields attached to objects like charges, customers, or payment intents to disappear during processing. This problem typically surfaces when your backend SDK uses one API version, but your Stripe webhook endpoint or account default is configured to an older or newer release. To resolve metadata loss, you must synchronize your webhook endpoint version with your server-side SDK version, ensure nested metadata schemas match the active version’s contract, or fetch full objects directly using a pinned API key.

Why does a Stripe API version mismatch cause metadata to disappear?

A Stripe API version mismatch drops metadata when an incoming webhook or downstream request is processed through an API version that structures, nests, or exposes object properties differently than the version used to create them. When Stripe serializes events for a webhook pinned to an incompatible version, custom metadata fields can be stripped or relocated within child properties.


Diagnostic Snapshot: API Version Mismatches

ParameterDiagnostic Value
Failure ModeEmpty metadata: {} dictionaries on retrieved objects or webhooks
Affected ComponentsWebhooks, PaymentIntents, Charges, Invoices, Subscriptions
Primary Root CauseMismatch between Dashboard default API version, webhook version, and SDK version
Detection PointStripe Dashboard > Developers > Events / Logs
Resolution TypeWebhook configuration update, SDK version pinning, or direct API re-fetching

Diagnostic Path: Identifying Where Metadata Disappears

Metadata missing in application workflow
  │
  ├──► Check Stripe Dashboard Event Log (Developers > Events)
  │      │
  │      ├── Metadata exists in the Dashboard Event JSON?
  │      │     ├── YES: Webhook payload transformation error or SDK version mismatch
  │      │     └── NO:  Metadata was omitted during object creation or overwritten upstream
  │      │
  │      └── Compare API Versions (Developers > Webhooks vs. Backend SDK)
  │            ├── Webhook version != Backend SDK version? ──► Payload serialized under older schema
  │            └── Webhook version == Backend SDK version? ──► Partial update wiped metadata dictionary
  │
  └──► Check API Logs for Overwrite Requests (/v1/payment_intents/:id)
         └── Empty metadata update ("metadata": {}) found in subsequent API call

Why API Version Mismatches Drop Metadata

Stripe regularly releases versioned updates to its API schema. Each version locks in how objects are shaped, how fields are nested, and how related data is expanded.

Your Stripe account has a global default API version, typically set to the date you registered your account. However, every programmatic request can specify a custom version using the Stripe-Version HTTP header. Similarly, each webhook endpoint you create in the Stripe Dashboard can be pinned to a specific API version independently of your account default.

When your application server creates a PaymentIntent using a modern SDK version (for example, pinned to a recent schema), you attach custom key-value pairs to the metadata parameter. If your webhook listener is configured to an older API version, Stripe serializes the webhook payload according to that older version’s exact contract.

In older Stripe API releases, metadata properties were occasionally nested within child objects rather than top-level parent resources, or certain event types did not include expanded child metadata by default. When your modern backend code attempts to read event.data.object.metadata.order_id, the field appears undefined or entirely blank because the payload was shaped by the legacy webhook version.


Common Scenarios and Root Causes

1. Webhook Endpoint Version Differs from Server SDK

This is the most common origin of metadata loss. Developers frequently upgrade the stripe package in package.json or requirements.txt. Upgrading the SDK shifts the server-side requests to a newer Stripe API version.

However, upgrading your local package does not update existing webhook endpoints in the Stripe Dashboard. When Stripe sends an event such as payment_intent.succeeded to your webhook URL, it serializes the data according to the version assigned to that specific endpoint. If the schema has changed between the two releases, fields your application expects to find at the top level may be relocated or omitted.

For a deeper dive into debugging endpoint data pipelines, see Fix: Stripe “Metadata” stripping during API transfer.

2. Destructive Metadata Updates (Key Overwriting)

Stripe metadata updates do not perform a recursive deep-merge. When you update a Stripe resource using the API, the metadata dictionary behaves as an explicit overwrite for the keys you send.

If your integration executes an update request to add a fulfillment status:

// Unintended metadata wipe
await stripe.paymentIntents.update(intentId, {
  metadata: { fulfillment_status: 'packed' }
});

If previous code attached order_id, customer_ref, and warehouse_id, those keys are permanently deleted unless they are re-sent in the same update call. Sending an empty object (metadata: {}) clears all stored metadata keys instantly.

3. PaymentIntent vs. Charge Level Metadata Isolation

Metadata set on a PaymentIntent does not automatically copy to all underlying Charge objects in legacy API versions. If your webhook listener processes charge.succeeded instead of payment_intent.succeeded, it examines the Charge object. In earlier Stripe API versions, if metadata was applied only to the PaymentIntent, the corresponding Charge object held an empty metadata dictionary.


Step-by-Step Resolution

Step 1: Align Webhook Versions in the Stripe Dashboard

To ensure your webhook endpoints deliver payloads matching your application’s expectations:

  1. Open the Stripe Dashboard and navigate to Developers > Webhooks.
  2. Select the webhook endpoint that receives your payment notifications.
  3. Check the API version displayed in the endpoint overview.
  4. If it differs from your backend SDK, click Update details or Upgrade version.
  5. Test the upgrade in your staging environment first, as altering the API version changes how timestamps, line items, and nested errors are structured across all events handled by that URL.

If your webhook receiver throws unhandled exceptions after version changes, review Troubleshooting: Stripe Webhook “500 Internal Server Error”.

Step 2: Explicitly Pin the API Version in Your Code

Never rely on the default account-level API version configured in the dashboard. Always define the version explicitly when initializing the Stripe client in your codebase.

Node.js Example:

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY, {
  apiVersion: '2024-06-20', // Pin explicitly to match your webhook endpoint
});

Python Example:

import stripe
stripe.api_key = os.environ.get("STRIPE_SECRET_KEY")
stripe.api_version = "2024-06-20" # Pin explicitly to match your webhook endpoint

Step 3: Implement Re-fetching for Asynchronous Workflows

If you operate in a complex microservices architecture where webhooks must support multiple disparate services on older versions, do not rely on the webhook event payload for business-critical metadata.

Instead, extract the resource identifier (such as pi_12345) from the event and immediately execute an authoritative GET request using your pinned server SDK:

// Inside your webhook handler
app.post('/webhook', async (req, res) => {
  const event = req.body;

  if (event.type === 'payment_intent.succeeded') {
    const rawIntent = event.data.object;

    // Authoritative fetch with your pinned SDK version
    const paymentIntent = await stripe.paymentIntents.retrieve(rawIntent.id);

    const orderId = paymentIntent.metadata.order_id;
    await completeOrder(orderId);
  }

  res.json({ received: true });
});

This guarantees that the object is parsed strictly using the version declared in your active server client, completely bypassing any serialization mismatches introduced by intermediate webhook proxies.


Final Validation Check

Before leaving your integration:

  • Verify that your API keys have sufficient access to read the relevant objects by confirming your setup against Fix: Stripe “Restricted Key” permissions blocking valid API calls.
  • Trigger a test transaction and inspect the raw request and response data under Developers > Logs in the Stripe Dashboard to confirm that all required metadata keys persist from creation to final settlement.