[GH-ISSUE #4104] [Stripe] Metadata not passed through to Stripe subscription on upgrade #18461

Closed
opened 2026-04-15 16:55:10 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @Lukem121 on GitHub (Aug 20, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/4104

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

When creating a subscription using the authClient.subscription.upgrade method, the metadata object does not get applied to the resulting Stripe subscription. Normally Stripe supports attaching metadata to subscriptions, but it seems this plugin does not pass the values through.

Steps to Reproduce:

  1. Call authClient.subscription.upgrade with a metadata object.
  2. Complete the hosted checkout flow.
  3. Check the created subscription in Stripe.
const { error: upgradeError } = await authClient.subscription.upgrade({
  plan,
  successUrl,
  cancelUrl,
  annual: isAnnual,
  metadata: {
    ga_client_id: clientId,
  },
  fetchOptions: {
    headers: {
      'x-currency': currency,
      'x-ga-client-id': clientId
    },
  },
});

I also attempted to add the metadata, initially passing it via the headers, and then again in the getCheckoutSessionParams hook as part of the metadata.

getCheckoutSessionParams: (_, request) => {
  const currency = request?.headers?.get('x-currency') ?? undefined;
  const clientId = request?.headers?.get('x-ga-client-id') ?? undefined;

  return {
    params: {
      allow_promotion_codes: true,
      currency,
      metadata: {
        test: 'test123',
        ga_client_id: clientId ?? 'no client id',
      },
    },
  };
},

Current vs. Expected behavior

Expected behavior:
The Stripe subscription should contain the metadata key/value pairs (e.g., ga_client_id) as provided in the upgrade call.

Actual behavior:
The subscription is created successfully, but the metadata field is missing on the subscription object in Stripe.

What version of Better Auth are you using?

1.3.7

System info

System:
    OS: Windows 11 10.0.26100
    CPU: (32) x64 AMD Ryzen 9 9950X3D 16-Core Processor
    Memory: 67.17 GB / 93.65 GB
  Browsers:
    Edge: Chromium (139.0.3405.86)

Which area(s) are affected? (Select all that apply)

Package

Auth config (if applicable)


Additional context

No response

Originally created by @Lukem121 on GitHub (Aug 20, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/4104 ### Is this suited for github? - [ ] Yes, this is suited for github ### To Reproduce When creating a subscription using the `authClient.subscription.upgrade` method, the `metadata` object does not get applied to the resulting Stripe subscription. Normally Stripe supports attaching metadata to subscriptions, but it seems this plugin does not pass the values through. Steps to Reproduce: 1. Call authClient.subscription.upgrade with a metadata object. 2. Complete the hosted checkout flow. 3. Check the created subscription in Stripe. ``` typescript const { error: upgradeError } = await authClient.subscription.upgrade({ plan, successUrl, cancelUrl, annual: isAnnual, metadata: { ga_client_id: clientId, }, fetchOptions: { headers: { 'x-currency': currency, 'x-ga-client-id': clientId }, }, }); ``` I also attempted to add the metadata, initially passing it via the headers, and then again in the `getCheckoutSessionParams` hook as part of the metadata. ``` typescript getCheckoutSessionParams: (_, request) => { const currency = request?.headers?.get('x-currency') ?? undefined; const clientId = request?.headers?.get('x-ga-client-id') ?? undefined; return { params: { allow_promotion_codes: true, currency, metadata: { test: 'test123', ga_client_id: clientId ?? 'no client id', }, }, }; }, ``` ### Current vs. Expected behavior Expected behavior: The Stripe subscription should contain the metadata key/value pairs (e.g., ga_client_id) as provided in the upgrade call. Actual behavior: The subscription is created successfully, but the metadata field is missing on the subscription object in Stripe. ### What version of Better Auth are you using? 1.3.7 ### System info ```bash System: OS: Windows 11 10.0.26100 CPU: (32) x64 AMD Ryzen 9 9950X3D 16-Core Processor Memory: 67.17 GB / 93.65 GB Browsers: Edge: Chromium (139.0.3405.86) ``` ### Which area(s) are affected? (Select all that apply) Package ### Auth config (if applicable) ```typescript ``` ### Additional context _No response_
GiteaMirror added the lockedbug labels 2026-04-15 16:55:10 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Aug 20, 2025):

Hi @Lukem121! I'm Dosu and I’m helping the better-auth team.

This is a known limitation: metadata passed to authClient.subscription.upgrade is only attached to the Stripe customer if a new customer is created, not to the subscription itself. By default, Better Auth does not forward metadata from the upgrade call to the Stripe subscription or Checkout Session, so you won't see your custom fields on the subscription in Stripe events or the dashboard (details).

To ensure your metadata appears on the Stripe subscription, you need to explicitly add it in the getCheckoutSessionParams option of your Stripe plugin config. However, the metadata you pass to subscription.upgrade is not automatically available in this hook—you'll need to persist it somewhere accessible (like the session, a temporary store, or your database), then retrieve it in getCheckoutSessionParams and include it in the metadata you send to Stripe (pattern explained).

Example:

stripe({
  // ...other options
  subscription: {
    getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => {
      // Retrieve your custom metadata from session, DB, etc.
      return {
        params: {
          metadata: {
            ga_client_id: /* your value here */,
            // ...other fields
          }
        }
      };
    }
  }
});

Better Auth does not store arbitrary metadata on subscriptions, so if you need to persist custom data locally, handle that separately. This topic is tracked in issue #3429 and related threads.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3207078104 --> @dosubot[bot] commented on GitHub (Aug 20, 2025): <!-- Greeting --> Hi @Lukem121! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a known limitation: metadata passed to `authClient.subscription.upgrade` is only attached to the Stripe customer if a new customer is created, not to the subscription itself. By default, Better Auth does not forward metadata from the upgrade call to the Stripe subscription or Checkout Session, so you won't see your custom fields on the subscription in Stripe events or the dashboard ([details](https://github.com/better-auth/better-auth/issues/3429#issuecomment-3083928940)). To ensure your metadata appears on the Stripe subscription, you need to explicitly add it in the `getCheckoutSessionParams` option of your Stripe plugin config. However, the metadata you pass to `subscription.upgrade` is not automatically available in this hook—you'll need to persist it somewhere accessible (like the session, a temporary store, or your database), then retrieve it in `getCheckoutSessionParams` and include it in the metadata you send to Stripe ([pattern explained](https://github.com/better-auth/better-auth/issues/3429#issuecomment-3084025795)). Example: ```typescript stripe({ // ...other options subscription: { getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => { // Retrieve your custom metadata from session, DB, etc. return { params: { metadata: { ga_client_id: /* your value here */, // ...other fields } } }; } } }); ``` Better Auth does not store arbitrary metadata on subscriptions, so if you need to persist custom data locally, handle that separately. This topic is tracked in [issue #3429](https://github.com/better-auth/better-auth/issues/3429) and related threads. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/ea8a33d1-1569-4ad2-bb3d-93cf9d6c56ac?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/ea8a33d1-1569-4ad2-bb3d-93cf9d6c56ac?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/ea8a33d1-1569-4ad2-bb3d-93cf9d6c56ac?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/ea8a33d1-1569-4ad2-bb3d-93cf9d6c56ac?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/ea8a33d1-1569-4ad2-bb3d-93cf9d6c56ac?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/ea8a33d1-1569-4ad2-bb3d-93cf9d6c56ac?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/ea8a33d1-1569-4ad2-bb3d-93cf9d6c56ac?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/4104)
Author
Owner

@Lukem121 commented on GitHub (Aug 20, 2025):

I've implemented a working solution for this limitation that others might find helpful.

My Approach
Since metadata from authClient.subscription.upgrade doesn't automatically flow to the Stripe subscription, we use a combination of headers and checkout session metadata extraction.

Step 1: Pass Metadata as Headers
When calling subscription.upgrade, we pass our metadata both in the metadata field AND as custom headers:

const { error: upgradeError } = await authClient.subscription.upgrade({
  plan,
  successUrl,
  cancelUrl,
  annual: isAnnual,
  metadata: {
    ga_client_id: clientId, // This won't make it to the subscription
  },
  fetchOptions: {
    headers: {
      'x-currency': currency,
      'x-ga-client-id': clientId, // This WILL be accessible in getCheckoutSessionParams
    },
  },
});

Step 2: Extract Headers in getCheckoutSessionParams
In our Stripe plugin config, we extract the headers and add them to the checkout session metadata:

stripe({
  // ...other options
  subscription: {
    getCheckoutSessionParams: (_, request) => {
      const clientId = request?.headers?.get('x-ga-client-id') ?? undefined;
      
      return {
        params: {
          allow_promotion_codes: true,
          metadata: {
            ga_client_id: clientId,
            // This metadata WILL be available on the checkout session
          },
        },
      };
    }
  }
});

Step 3: Extract Metadata from Checkout Session in Webhooks
The key insight: the metadata appears on the checkout session, not the subscription. In our webhook handlers (e.g., subscription completion), we extract it from the checkout session:

const extractClientId = (
  event: Stripe.Event, 
  stripeSubscription: Stripe.Subscription
): string | undefined => {
  // If this event is from a checkout session completion, get metadata from there
  if (event.type === 'checkout.session.completed' && event.data?.object) {
    const checkoutSession = event.data.object as Stripe.Checkout.Session;
    return checkoutSession.metadata?.ga_client_id;
  }
  
  // Fallback to subscription metadata (likely undefined due to this issue)
  return stripeSubscription.metadata?.ga_client_id;
};

// Usage in webhook handler
export const onSubscriptionComplete = async ({ event, subscription, plan, stripeSubscription }) => {
  const clientId = extractClientId(event, stripeSubscription);
  // Now you have access to your metadata!
};
<!-- gh-comment-id:3207325448 --> @Lukem121 commented on GitHub (Aug 20, 2025): I've implemented a working solution for this limitation that others might find helpful. My Approach Since metadata from `authClient.subscription.upgrade` doesn't automatically flow to the Stripe subscription, we use a combination of headers and checkout session `metadata` extraction. Step 1: Pass Metadata as Headers When calling `subscription.upgrade`, we pass our metadata both in the `metadata` field AND as custom headers: ```typescript const { error: upgradeError } = await authClient.subscription.upgrade({ plan, successUrl, cancelUrl, annual: isAnnual, metadata: { ga_client_id: clientId, // This won't make it to the subscription }, fetchOptions: { headers: { 'x-currency': currency, 'x-ga-client-id': clientId, // This WILL be accessible in getCheckoutSessionParams }, }, }); ``` Step 2: Extract Headers in getCheckoutSessionParams In our Stripe plugin config, we extract the headers and add them to the checkout session metadata: ```typescript stripe({ // ...other options subscription: { getCheckoutSessionParams: (_, request) => { const clientId = request?.headers?.get('x-ga-client-id') ?? undefined; return { params: { allow_promotion_codes: true, metadata: { ga_client_id: clientId, // This metadata WILL be available on the checkout session }, }, }; } } }); ``` Step 3: Extract Metadata from Checkout Session in Webhooks The key insight: the metadata appears on the checkout session, not the subscription. In our webhook handlers (e.g., subscription completion), we extract it from the checkout session: ```typescript const extractClientId = ( event: Stripe.Event, stripeSubscription: Stripe.Subscription ): string | undefined => { // If this event is from a checkout session completion, get metadata from there if (event.type === 'checkout.session.completed' && event.data?.object) { const checkoutSession = event.data.object as Stripe.Checkout.Session; return checkoutSession.metadata?.ga_client_id; } // Fallback to subscription metadata (likely undefined due to this issue) return stripeSubscription.metadata?.ga_client_id; }; // Usage in webhook handler export const onSubscriptionComplete = async ({ event, subscription, plan, stripeSubscription }) => { const clientId = extractClientId(event, stripeSubscription); // Now you have access to your metadata! }; ```
Author
Owner

@dosubot[bot] commented on GitHub (Nov 19, 2025):

Hi, @Lukem121. I'm Dosu, and I'm helping the better-auth team manage their backlog and am marking this issue as stale.

Issue Summary:

  • You reported that metadata passed to authClient.subscription.upgrade was not forwarded to the resulting Stripe subscription in Better Auth v1.3.7.
  • It was clarified that metadata only attaches to new Stripe customers, not subscriptions, by default.
  • A workaround was shared where metadata is passed via custom headers during upgrade, extracted in the Stripe plugin's getCheckoutSessionParams hook, and retrieved from checkout session metadata in webhook handlers.
  • This approach enables access to the desired metadata despite the limitation.

Next Steps:

  • Please let me know if this issue is still relevant with the latest version of better-auth by commenting here.
  • Otherwise, I will automatically close this issue in 7 days.

Thank you for your understanding and contribution!

<!-- gh-comment-id:3553559794 --> @dosubot[bot] commented on GitHub (Nov 19, 2025): Hi, @Lukem121. I'm [Dosu](https://dosu.dev), and I'm helping the better-auth team manage their backlog and am marking this issue as stale. **Issue Summary:** - You reported that metadata passed to `authClient.subscription.upgrade` was not forwarded to the resulting Stripe subscription in Better Auth v1.3.7. - It was clarified that metadata only attaches to new Stripe customers, not subscriptions, by default. - A workaround was shared where metadata is passed via custom headers during upgrade, extracted in the Stripe plugin's `getCheckoutSessionParams` hook, and retrieved from checkout session metadata in webhook handlers. - This approach enables access to the desired metadata despite the limitation. **Next Steps:** - Please let me know if this issue is still relevant with the latest version of better-auth by commenting here. - Otherwise, I will automatically close this issue in 7 days. Thank you for your understanding and contribution!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#18461