cancelAtPeriodEnd doesn't update to true when using better-auth method #2810

Closed
opened 2026-03-13 10:21:35 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @dani-z on GitHub (Feb 2, 2026).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Create a subscription
  2. Cancel sub using better-auth on the server auth.api.cancelSubscription()
  3. cancelAtPeriodEnd remains false in DB, however cancelAt field is populated with the correct date (image attached)
  4. When manually cancelling from Stripe the cancelAtPeriodEnd correctly switches to true
Image

Code used to cancel sub

auth.api.cancelSubscription({
  body: {
    referenceId: userId,
    customerType: "user", // TODO: This will change to "organization" when we have organizations
    subscriptionId: activeSubscription.stripeSubscriptionId,
    returnUrl: `${env.NEXT_PUBLIC_APP_URL}/dashboard/billing`,
  },
  headers: await headers(),
})

Current vs. Expected behavior

Current: cancelAtPeriodEnd does not update to true better-auth server method
Expected: cancelAtPeriodEnd should update to true when using better-auth server method

What version of Better Auth are you using?

1.4.18

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.6.0: Wed Nov  5 21:32:34 PST 2025; root:xnu-11417.140.69.705.2~1/RELEASE_ARM64_T6020",
    "release": "24.6.0",
    "cpuCount": 12,
    "cpuModel": "Apple M2 Max",
    "totalMemory": "64.00 GB",
    "freeMemory": "0.44 GB"
  },
  "node": {
    "version": "v24.3.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.6.0"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "16.1.6"
    },
    {
      "name": "react",
      "version": "19.2.4"
    }
  ],
  "databases": [
    {
      "name": "@prisma/client",
      "version": "^7.3.0"
    }
  ],
  "betterAuth": {
    "version": "Unknown",
    "config": null,
    "error": "Converting circular structure to JSON\n    --> starting at object with constructor 'Stripe'\n    |     property 'account' -> object with constructor 'Constructor'\n    --- property '_stripe' closes the circle"
  }

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

Backend

Auth config (if applicable)

export const auth = betterAuth({
  baseURL: env.NEXT_PUBLIC_APP_URL,
  database: prismaAdapter(prisma, {
    provider: "postgresql",
  }),
  experimental: { joins: true },
  emailAndPassword: {
    enabled: true,
    autoSignIn: true,
    requireEmailVerification: true,
  },
  socialProviders: {
    google: {
      clientId: env.GOOGLE_CLIENT_ID,
      clientSecret: env.GOOGLE_CLIENT_SECRET,
    },
  },
  user: {
    additionalFields: {
      companyName: {
        type: "string",
        required: false,
      },
      timeZone: {
        type: "string",
        required: false,
        defaultValue: "UTC",
      },
      onboardingComplete: {
        type: "boolean",
        required: false,
        defaultValue: false,
      },
      deletedAt: {
        type: "date",
        required: false,
        input: false,
      },
      purgeAt: {
        type: "date",
        required: false,
        input: false,
      },
    },
    deleteUser: {
      enabled: true,
    },
  },
  databaseHooks: {
    session: {
      create: {
        before: async (session) => {
          if (!session.userId) return;
          const deleted = await isUserDeleted({ userId: session.userId });
          if (deleted) {
            return false;
          }
        },
      },
    },
  },
  plugins: [
    stripe({
      stripeClient,
      stripeWebhookSecret: env.STRIPE_WEBHOOK_SECRET,
      createCustomerOnSignUp: true,
      subscription: {
        enabled: true,
        plans: [
          {
            name: "PRO",
            priceId: env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID,
            annualDiscountPriceId: env.NEXT_PUBLIC_STRIPE_PRO_ANNUAL_PRICE_ID,
          },
          {
            name: "BUSINESS",
            priceId: env.NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID,
            annualDiscountPriceId:
              env.NEXT_PUBLIC_STRIPE_BUSINESS_ANNUAL_PRICE_ID,
          },
          {
            name: "ENTERPRISE",
            priceId: env.NEXT_PUBLIC_STRIPE_ENTERPRISE_MONTHLY_PRICE_ID,
            annualDiscountPriceId:
              env.NEXT_PUBLIC_STRIPE_ENTERPRISE_ANNUAL_PRICE_ID,
          },
        ],
        getCheckoutSessionParams() {
          return {
            params: {
              allow_promotion_codes: true,
            },
          };
        },
      },
    }),
    lastLoginMethod(),
    nextCookies(),
  ],
});

export type Session = typeof auth.$Infer.Session;

Additional context

No response

Originally created by @dani-z on GitHub (Feb 2, 2026). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Create a subscription 2. Cancel sub using better-auth on the server `auth.api.cancelSubscription()` 3. `cancelAtPeriodEnd` remains `false` in DB, however `cancelAt` field is populated with the correct date (image attached) 4. When manually cancelling from Stripe the `cancelAtPeriodEnd` correctly switches to `true` <img width="439" height="103" alt="Image" src="https://github.com/user-attachments/assets/f2c1920e-f621-48b6-9892-8162c8c4a91a" /> Code used to cancel sub ```typescript auth.api.cancelSubscription({ body: { referenceId: userId, customerType: "user", // TODO: This will change to "organization" when we have organizations subscriptionId: activeSubscription.stripeSubscriptionId, returnUrl: `${env.NEXT_PUBLIC_APP_URL}/dashboard/billing`, }, headers: await headers(), }) ``` ### Current vs. Expected behavior Current: `cancelAtPeriodEnd` does not update to `true` better-auth server method Expected: `cancelAtPeriodEnd` should update to `true` when using better-auth server method ### What version of Better Auth are you using? 1.4.18 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.6.0: Wed Nov 5 21:32:34 PST 2025; root:xnu-11417.140.69.705.2~1/RELEASE_ARM64_T6020", "release": "24.6.0", "cpuCount": 12, "cpuModel": "Apple M2 Max", "totalMemory": "64.00 GB", "freeMemory": "0.44 GB" }, "node": { "version": "v24.3.0", "env": "development" }, "packageManager": { "name": "npm", "version": "11.6.0" }, "frameworks": [ { "name": "next", "version": "16.1.6" }, { "name": "react", "version": "19.2.4" } ], "databases": [ { "name": "@prisma/client", "version": "^7.3.0" } ], "betterAuth": { "version": "Unknown", "config": null, "error": "Converting circular structure to JSON\n --> starting at object with constructor 'Stripe'\n | property 'account' -> object with constructor 'Constructor'\n --- property '_stripe' closes the circle" } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript export const auth = betterAuth({ baseURL: env.NEXT_PUBLIC_APP_URL, database: prismaAdapter(prisma, { provider: "postgresql", }), experimental: { joins: true }, emailAndPassword: { enabled: true, autoSignIn: true, requireEmailVerification: true, }, socialProviders: { google: { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET, }, }, user: { additionalFields: { companyName: { type: "string", required: false, }, timeZone: { type: "string", required: false, defaultValue: "UTC", }, onboardingComplete: { type: "boolean", required: false, defaultValue: false, }, deletedAt: { type: "date", required: false, input: false, }, purgeAt: { type: "date", required: false, input: false, }, }, deleteUser: { enabled: true, }, }, databaseHooks: { session: { create: { before: async (session) => { if (!session.userId) return; const deleted = await isUserDeleted({ userId: session.userId }); if (deleted) { return false; } }, }, }, }, plugins: [ stripe({ stripeClient, stripeWebhookSecret: env.STRIPE_WEBHOOK_SECRET, createCustomerOnSignUp: true, subscription: { enabled: true, plans: [ { name: "PRO", priceId: env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID, annualDiscountPriceId: env.NEXT_PUBLIC_STRIPE_PRO_ANNUAL_PRICE_ID, }, { name: "BUSINESS", priceId: env.NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PRICE_ID, annualDiscountPriceId: env.NEXT_PUBLIC_STRIPE_BUSINESS_ANNUAL_PRICE_ID, }, { name: "ENTERPRISE", priceId: env.NEXT_PUBLIC_STRIPE_ENTERPRISE_MONTHLY_PRICE_ID, annualDiscountPriceId: env.NEXT_PUBLIC_STRIPE_ENTERPRISE_ANNUAL_PRICE_ID, }, ], getCheckoutSessionParams() { return { params: { allow_promotion_codes: true, }, }; }, }, }), lastLoginMethod(), nextCookies(), ], }); export type Session = typeof auth.$Infer.Session; ``` ### Additional context _No response_
GiteaMirror added the bug label 2026-03-13 10:21:35 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Feb 2, 2026):

I understand you're having issues with cancelAtPeriodEnd not updating to true when using Better Auth's method auth.api.cancelSubscription(). This is a known issue that has been addressed in open discussions in the Better Auth community. Users have reported similar behavior where the cancelAtPeriodEnd remains false, even though the correct date is set in the cancelAt field upon cancellation via the API[1].

It seems that this could be a potential regression in version 1.4.18, as mentioned by a user who experienced similar problems and found that downgrading to 1.4.17 resolved the issue[2]. You might consider temporarily downgrading if this functionality is critical for you, while awaiting a patch.

For further assistance, it may also help to tag members of the Better Auth team, like Beka and Max, with @bekacru and @ping__, respectively, for more tailored support.

Is there anything specific you would like help with regarding subscriptions or the Better Auth API?

If you need more help, tag @better-auth-agent in a comment so I can respond.

DiagramDiscordGitHub

Diagram Join Star

@better-auth-agent[bot] commented on GitHub (Feb 2, 2026): I understand you're having issues with `cancelAtPeriodEnd` not updating to `true` when using Better Auth's method `auth.api.cancelSubscription()`. This is a known issue that has been addressed in open discussions in the Better Auth community. Users have reported similar behavior where the `cancelAtPeriodEnd` remains `false`, even though the correct date is set in the `cancelAt` field upon cancellation via the API[[1]](https://github.com/better-auth/better-auth/issues/5780). It seems that this could be a potential regression in version 1.4.18, as mentioned by a user who experienced similar problems and found that downgrading to 1.4.17 resolved the issue[[2]](https://www.better-auth.com/docs/plugins/stripe). You might consider temporarily downgrading if this functionality is critical for you, while awaiting a patch. For further assistance, it may also help to tag members of the Better Auth team, like Beka and Max, with @bekacru and @ping__, respectively, for more tailored support. Is there anything specific you would like help with regarding subscriptions or the Better Auth API? _If you need more help, tag @better-auth-agent in a comment so I can respond._ <!-- 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

@bytaesu commented on GitHub (Feb 3, 2026):

Image

Hi @dani-z,

Previously, we had modified in between, and this PR improved that.

Canceling from the dashboard and canceling via the billing portal return different values, which may be why it appeared that way on your end. It also varies depending on whether the cancellation is immediate or scheduled.

Each Stripe status field has its own meaning, and we now sync it exactly as received from Stripe webhooks.

@bytaesu commented on GitHub (Feb 3, 2026): <img width="600" alt="Image" src="https://github.com/user-attachments/assets/ad2a6831-3687-452c-88ad-dd2cd4f28cba" /> --- Hi @dani-z, Previously, we had modified in between, and [this PR](https://github.com/better-auth/better-auth/pull/6961) improved that. Canceling from the dashboard and canceling via the billing portal return different values, which may be why it appeared that way on your end. It also varies depending on whether the cancellation is immediate or scheduled. Each Stripe status field has its own meaning, and we now sync it exactly as received from Stripe webhooks.
Author
Owner

@dani-z commented on GitHub (Feb 4, 2026):

Thanks for the info @bytaesu, I saw that in docs but didn't really understand it. In this case is it safe to just use the cancelAt datetime field to show / hide some messages instead of the boolean cancelAtPeriodEnd?

I just opened this as a bug as I thought cancelling from the API should work the same as cancelling from the Stripe portal. If that's not the case this can be closed as it is not a bug.

@dani-z commented on GitHub (Feb 4, 2026): Thanks for the info @bytaesu, I saw that in docs but didn't really understand it. In this case is it safe to just use the `cancelAt` datetime field to show / hide some messages instead of the boolean `cancelAtPeriodEnd`? I just opened this as a bug as I thought cancelling from the API should work the same as cancelling from the Stripe portal. If that's not the case this can be closed as it is not a bug.
Author
Owner

@bytaesu commented on GitHub (Feb 4, 2026):

Hi @dani-z,

cancelAt alone isn't reliable, since cancelAtPeriodEnd can be set while cancelAt is null. To check if a subscription is scheduled for cancellation, you need to check both cancelAtPeriodEnd and cancelAt.

This isn’t unique behavior of Better Auth, refer to the Stripe docs.

Checking the webhook logs will make it clear 🙂

@bytaesu commented on GitHub (Feb 4, 2026): Hi @dani-z, `cancelAt` alone isn't reliable, since `cancelAtPeriodEnd` can be set while `cancelAt` is null. To check if a subscription is scheduled for cancellation, you need to check both `cancelAtPeriodEnd` and `cancelAt`. This isn’t unique behavior of Better Auth, refer to the Stripe docs. Checking the webhook logs will make it clear 🙂
Author
Owner

@bytaesu commented on GitHub (Feb 4, 2026):

I'm closing this, Feel free to mention me if you have further questions!

@bytaesu commented on GitHub (Feb 4, 2026): I'm closing this, Feel free to mention me if you have further questions!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2810