[GH-ISSUE #4565] Stripe webhook couldn't handle checkout session "payment" mode #18615

Open
opened 2026-04-15 17:10:06 -05:00 by GiteaMirror · 6 comments
Owner

Originally created by @akmalmzamri on GitHub (Sep 10, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/4565

Originally assigned to: @bytaesu on GitHub.

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Hi. I'm currently using Better Auth's Stripe plugin to handle the subscription for my SaaS. Works great. Really love the DX improvement!

I want to add a "purchase additional credits" feature. This is a one-off payment and requires checkout with payment mode.

export async function POST(request: NextRequest) {
  const session = await auth.api.getSession({ headers: request.headers })
  const subscriptions = await auth.api.listActiveSubscriptions({ headers: request.headers })
  const stripeCustomerId = subscriptions[0].stripeCustomerId

  if (!session) {
    return new Response("Unauthorized", { status: 401 })
  }

  try {
    const checkoutSession = await stripe.checkout.sessions.create({
      mode: "payment",
      customer: stripeCustomerId,
      line_items: [
        {
          price: payment.creditTopup.priceId,
          quantity: 1,
        },
      ],
      success_url: `http://localhost:3000/app/user`,
      cancel_url: `http://localhost:3000/app/user`,
      // }
    })

    return Response.json({ url: checkoutSession.url })
  } catch (error) {
    console.error("Error creating checkout session:", error)
    return new Response("Internal Server Error", { status: 500 })
  }
}

and since there's no built-in hook to handle the event, we have to handle it manually

stripeConfig({
  // other stuff
  onEvent: async (event) => {
    switch (event.type) {
      case "checkout.session.completed":
        const checkoutSession = event.data.object
        if (checkoutSession.mode === "payment") {
          // do stuff
        }
        break
    }
  }
})

It works (kinda), but we'll get this error

ERROR [Better Auth]: Stripe webhook failed. Error: Stripe: Argument "subscription_exposed_id" must be a string, but got: null (on API request to GET /v1/subscriptions/{subscription_exposed_id})

which I assume because the payment mode is not handled in the plugin's onCheckoutSessionCompleted hook

I think this is a common feature for SaaS that uses a credit system, so it'd be great for this to be looked into =)

Current vs. Expected behavior

Current behavior

checkout.session.completed webhook event partially failed when the mode is set to payment. The subsequent onEvent is still executed, so the side effect can still successfully be triggered.

Expected behavior

checkout.session.completed should be executed without any error when the mode is set to payment

What version of Better Auth are you using?

1.3.9

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:30 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6020",
    "release": "24.6.0",
    "cpuCount": 10,
    "cpuModel": "Apple M2 Pro",
    "totalMemory": "16.00 GB",
    "freeMemory": "0.11 GB"
  },
  "node": {
    "version": "v24.2.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.3.0"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "15.4.5"
    },
    {
      "name": "react",
      "version": "^19.1.0"
    }
  ],
  "databases": [
    {
      "name": "postgres",
      "version": "^3.4.7"
    },
    {
      "name": "drizzle",
      "version": "0.44.4"
    }
  ],
  "betterAuth": {
    "version": "0.1.0",
    "config": null
  }
}

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

Backend, Package

Auth config (if applicable)


Additional context

No response

Originally created by @akmalmzamri on GitHub (Sep 10, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/4565 Originally assigned to: @bytaesu on GitHub. ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce Hi. I'm currently using Better Auth's Stripe plugin to handle the subscription for my SaaS. Works great. Really love the DX improvement! I want to add a "purchase additional credits" feature. This is a one-off payment and requires checkout with `payment` mode. ``` export async function POST(request: NextRequest) { const session = await auth.api.getSession({ headers: request.headers }) const subscriptions = await auth.api.listActiveSubscriptions({ headers: request.headers }) const stripeCustomerId = subscriptions[0].stripeCustomerId if (!session) { return new Response("Unauthorized", { status: 401 }) } try { const checkoutSession = await stripe.checkout.sessions.create({ mode: "payment", customer: stripeCustomerId, line_items: [ { price: payment.creditTopup.priceId, quantity: 1, }, ], success_url: `http://localhost:3000/app/user`, cancel_url: `http://localhost:3000/app/user`, // } }) return Response.json({ url: checkoutSession.url }) } catch (error) { console.error("Error creating checkout session:", error) return new Response("Internal Server Error", { status: 500 }) } } ``` and since there's no built-in hook to handle the event, we have to handle it manually ``` stripeConfig({ // other stuff onEvent: async (event) => { switch (event.type) { case "checkout.session.completed": const checkoutSession = event.data.object if (checkoutSession.mode === "payment") { // do stuff } break } } }) ``` It works (kinda), but we'll get this error > ERROR [Better Auth]: Stripe webhook failed. Error: Stripe: Argument "subscription_exposed_id" must be a string, but got: null (on API request to `GET /v1/subscriptions/{subscription_exposed_id}`) which I assume because the `payment` mode is not handled in the plugin's [onCheckoutSessionCompleted hook](https://github.com/better-auth/better-auth/blob/5b784698641bc68d7ad3ea537e6d564a44e60878/packages/stripe/src/hooks.ts#L14C3-L16C4) I think this is a common feature for SaaS that uses a credit system, so it'd be great for this to be looked into =) ### Current vs. Expected behavior **Current behavior** `checkout.session.completed` webhook event partially failed when the mode is set to `payment`. The subsequent `onEvent` is still executed, so the side effect can still successfully be triggered. **Expected behavior** `checkout.session.completed` should be executed without any error when the mode is set to `payment` ### What version of Better Auth are you using? 1.3.9 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:30 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6020", "release": "24.6.0", "cpuCount": 10, "cpuModel": "Apple M2 Pro", "totalMemory": "16.00 GB", "freeMemory": "0.11 GB" }, "node": { "version": "v24.2.0", "env": "development" }, "packageManager": { "name": "npm", "version": "11.3.0" }, "frameworks": [ { "name": "next", "version": "15.4.5" }, { "name": "react", "version": "^19.1.0" } ], "databases": [ { "name": "postgres", "version": "^3.4.7" }, { "name": "drizzle", "version": "0.44.4" } ], "betterAuth": { "version": "0.1.0", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Backend, Package ### Auth config (if applicable) ```typescript ``` ### Additional context _No response_
GiteaMirror added the paymentsbug labels 2026-04-15 17:10:07 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Sep 10, 2025):

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

You're correct—the error happens because the Better Auth Stripe plugin's webhook handler always tries to fetch a subscription for every checkout.session.completed event, regardless of the session mode. For one-off payments (mode: "payment"), there is no subscription, so the handler throws an error when it tries to access a null subscription ID. This is hardcoded in the onCheckoutSessionCompleted hook and isn't currently handled gracefully for payment mode sessions (source).

As a workaround, you can safely ignore this error since your custom onEvent handler still runs after the core handler fails. If you want to avoid the error log and unnecessary processing, you could patch your local copy of the plugin to add an early return in onCheckoutSessionCompleted for mode === "payment", like this:

if (checkoutSession.mode === "setup" || checkoutSession.mode === "payment" || !options.subscription?.enabled) {
  return;
}

This will prevent the plugin from trying to fetch a subscription for payment sessions and eliminate the error.

Currently, there is no official support for handling Stripe checkout sessions in payment mode in Better Auth, and recent updates have not added this feature (source). Your use-case (credits/one-off payments) is common, so this would be a valuable feature request for the project. For now, the workaround above should keep your logs clean and your custom logic working.

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:3275943743 --> @dosubot[bot] commented on GitHub (Sep 10, 2025): <!-- Greeting --> Hi @akmalmzamri! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> You're correct—the error happens because the Better Auth Stripe plugin's webhook handler always tries to fetch a subscription for every checkout.session.completed event, regardless of the session mode. For one-off payments (`mode: "payment"`), there is no subscription, so the handler throws an error when it tries to access a null subscription ID. This is hardcoded in the `onCheckoutSessionCompleted` hook and isn't currently handled gracefully for payment mode sessions [(source)](https://github.com/better-auth/better-auth/blob/84c5f563af9a019ae33138ff612646e1d760abdb/packages/stripe/src/hooks.ts). As a workaround, you can safely ignore this error since your custom `onEvent` handler still runs after the core handler fails. If you want to avoid the error log and unnecessary processing, you could patch your local copy of the plugin to add an early return in `onCheckoutSessionCompleted` for `mode === "payment"`, like this: ```ts if (checkoutSession.mode === "setup" || checkoutSession.mode === "payment" || !options.subscription?.enabled) { return; } ``` This will prevent the plugin from trying to fetch a subscription for payment sessions and eliminate the error. Currently, there is no official support for handling Stripe checkout sessions in payment mode in Better Auth, and recent updates have not added this feature [(source)](https://github.com/better-auth/better-auth/pull/4121). Your use-case (credits/one-off payments) is common, so this would be a valuable feature request for the project. For now, the workaround above should keep your logs clean and your custom logic working. <!-- 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/3acb0f4e-5398-4cf6-bb25-f82db00125d5?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/3acb0f4e-5398-4cf6-bb25-f82db00125d5?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/3acb0f4e-5398-4cf6-bb25-f82db00125d5?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/3acb0f4e-5398-4cf6-bb25-f82db00125d5?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/3acb0f4e-5398-4cf6-bb25-f82db00125d5?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/3acb0f4e-5398-4cf6-bb25-f82db00125d5?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/3acb0f4e-5398-4cf6-bb25-f82db00125d5?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/4565)
Author
Owner

@better-auth-agent[bot] commented on GitHub (Nov 3, 2025):

The error occurs because the built‐in hook for handling checkout.session.completed isn’t set up to properly process sessions in payment mode. In payment mode no subscription ID is provided, so the plugin ends up trying to fetch a subscription with a null identifier. To address this, you have a couple of options:

  1. Until the official fix is released, adjust your event handler (inside onEvent) so that when checkout sessions have mode "payment," you bypass the code that attempts to look up the subscription_exposed_id. This avoids the plugin’s default logic that expects a subscription to be present.

  2. Track the progress of the pending PR (which has already been created to add one-time payment support) and update your dependency when it’s merged. That PR will add proper handling for payment mode so that no error is thrown and the webhook completes successfully.

These steps should help you either patch around the issue in the meantime or update to a version that officially supports payment mode for one-off credit purchases [1], [2].

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3478676155 --> @better-auth-agent[bot] commented on GitHub (Nov 3, 2025): The error occurs because the built‐in hook for handling checkout.session.completed isn’t set up to properly process sessions in payment mode. In payment mode no subscription ID is provided, so the plugin ends up trying to fetch a subscription with a null identifier. To address this, you have a couple of options: 1. Until the official fix is released, adjust your event handler (inside onEvent) so that when checkout sessions have mode "payment," you bypass the code that attempts to look up the subscription_exposed_id. This avoids the plugin’s default logic that expects a subscription to be present. 2. Track the progress of the pending PR (which has already been created to add one-time payment support) and update your dependency when it’s merged. That PR will add proper handling for payment mode so that no error is thrown and the webhook completes successfully. These steps should help you either patch around the issue in the meantime or update to a version that officially supports payment mode for one-off credit purchases [[1]](https://github.com/better-auth/better-auth/issues/4565), [[2]](https://github.com/better-auth/better-auth/pull/4892). <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Author
Owner

@victorymakes commented on GitHub (Nov 3, 2025):

There was already a PR here for this use case.
https://github.com/better-auth/better-auth/pull/4892

<!-- gh-comment-id:3479009444 --> @victorymakes commented on GitHub (Nov 3, 2025): There was already a PR here for this use case. https://github.com/better-auth/better-auth/pull/4892
Author
Owner

@better-auth-agent[bot] commented on GitHub (Nov 3, 2025):

The error occurs because the built‐in onCheckoutSessionCompleted handler expects a subscription to be present, and in payment mode there isn’t one—so it ends up attempting to look up a subscription using a null value. A fix for this has already been proposed via a pull request that adjusts the hook logic to correctly handle sessions in payment mode [1].

Next steps:

  1. Update to the latest version of Better Auth (or apply the patch from the PR) so that the onCheckoutSessionCompleted hook safely skips or handles payment mode sessions.
  2. As a workaround, you can override the event handler with a custom onEvent implementation that, upon detecting session.mode === "payment", prevents any subscription fetching and instead runs your credit-purchase logic without error.

These steps should ensure that checkout.session.completed events in payment mode are handled without triggering a lookup for a null subscription ID [1], [2].

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3479010394 --> @better-auth-agent[bot] commented on GitHub (Nov 3, 2025): The error occurs because the built‐in onCheckoutSessionCompleted handler expects a subscription to be present, and in payment mode there isn’t one—so it ends up attempting to look up a subscription using a null value. A fix for this has already been proposed via a pull request that adjusts the hook logic to correctly handle sessions in payment mode [[1]](https://github.com/better-auth/better-auth/issues/4565). Next steps: 1. Update to the latest version of Better Auth (or apply the patch from the PR) so that the onCheckoutSessionCompleted hook safely skips or handles payment mode sessions. 2. As a workaround, you can override the event handler with a custom onEvent implementation that, upon detecting session.mode === "payment", prevents any subscription fetching and instead runs your credit-purchase logic without error. These steps should ensure that checkout.session.completed events in payment mode are handled without triggering a lookup for a null subscription ID [[1]](https://github.com/better-auth/better-auth/issues/4565), [[2]](https://github.com/better-auth/better-auth/pull/4892). <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Author
Owner

@devnaumov commented on GitHub (Jan 24, 2026):

Is it possible to just add if (mode === "payment") return at the plugin level?

<!-- gh-comment-id:3793063777 --> @devnaumov commented on GitHub (Jan 24, 2026): Is it possible to just add `if (mode === "payment") return` at the plugin level?
Author
Owner

@bytaesu commented on GitHub (Jan 28, 2026):

Stripe plugin is focused on subscriptions for now. We are considering adding payments in the future 🙂

<!-- gh-comment-id:3808638066 --> @bytaesu commented on GitHub (Jan 28, 2026): Stripe plugin is focused on subscriptions for now. We are considering adding payments in the future 🙂
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#18615