Stripe Webhook Fails When Processing User Subscription #961

Closed
opened 2026-03-13 08:12:14 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @ayrtonaguiar on GitHub (Apr 2, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Create a new user subscription via Stripe.
  2. Wait for the Stripe webhook to be triggered and attempt to update the subscription in the database.
  3. Check the server logs for the error.

Current vs. Expected behavior

Current Behavior:
The webhook fails due to invalid values for periodStart and periodEnd.

2025-04-02T12:18:50.775Z ERROR [Better Auth]: Stripe webhook failed. Error:
Invalid db[getModelName(model)].update() invocation in
C:\Users\ayrto\tibiaprofit.next\server\chunks\node_modules_better-auth_dist_e7fe42cb._.js:3150:62

3147 }
3148 const whereClause = convertWhereClause(model, where);
3149 const transformed = transformInput(update, model, "update");
→ 3150 const result = await db[getModelName(model)].update({
where: {
id: "wLFRtVgXFAouHdu8QflWJtfLBQpo7dBL"
},
data: {
plan: "explorer",
stripeSubscriptionId: "sub_1R9QFoGfZ5Enq2zqaMkwILzy",
status: "trialing",
periodStart: new Date("Invalid Date"),
~~~~~~~~~~~~~~~~~~~~~~~~
periodEnd: new Date("Invalid Date"),
seats: 1
}
})

Invalid value for argument periodStart: Provided Date object is invalid. Expected Date.

Expected Behavior:
The webhook should successfully process the user's subscription update, ensuring periodStart and periodEnd are valid dates.

What version of Better Auth are you using?

1.2.4

Provide environment information

"@better-auth/stripe": "^1.2.5"
"@prisma/client": "^6.5.0"
"better-auth": "^1.2.4"
"stripe": "^18.0.0"

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

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import prisma from "./prisma";
import { nextCookies } from "better-auth/next-js";
import { admin, customSession } from "better-auth/plugins";
import Stripe from "stripe";
import { stripe } from "@better-auth/stripe";

const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!, {
    apiVersion: '2025-03-31.basil',
    typescript: true,
});

export const auth = betterAuth({
    database: prismaAdapter(prisma, {
        provider: "postgresql",
    }),
    plugins: [
        stripe({
            stripeClient,
            stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
            createCustomerOnSignUp: true,
            subscription: {
                enabled: true,
                plans: [
                    {
                        name: "explorer",
                        priceId: "price_1R9GGmGfZ5Enq2zqo5hHqPSn",
                        freeTrial: {
                            days: 7,
                        },
                        limits: {
                            characters: 1,
                        },
                    }
                ]
            }
        }),
        customSession(
            async ({ session, user }) => {
                const userWithActiveCharacter = await prisma.user.findFirst({
                    where: { id: user.id },
                    include: {
                        activeCharacter: true,
                    },
                });

                return { user, session, activeCharacter: userWithActiveCharacter?.activeCharacter };
            }
        ),
        admin(),
        nextCookies()
    ],
    socialProviders: {
        google: {
            clientId: process.env.GOOGLE_CLIENT_ID as string,
            clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
        },
        discord: {
            clientId: process.env.DISCORD_CLIENT_ID as string,
            clientSecret: process.env.DISCORD_CLIENT_SECRET as string,
        },
    },
    user: {
        additionalFields: {
            activeCharacterId: {
                type: "string",
                required: false,
                defaultValue: null,
            },
            language: {
                type: "string",
                required: false,
                defaultValue: "en",
            },
        }
    },
});

Additional context

schema.prisma

model User {
  id            String      @id @default(uuid())
  name          String
  email         String
  emailVerified Boolean
  image         String?
  language      String?
  role          String?
  banned        Boolean?
  banReason     String?
  banExpires    DateTime?
  createdAt     DateTime    @default(now())
  updatedAt     DateTime    @updatedAt
  sessions      Session[]
  accounts      Account[]

  stripeCustomerId String?

  @@unique([email])
  @@map("users")
}

model Session {
  id        String   @id @default(uuid())
  expiresAt DateTime
  token     String
  createdAt DateTime
  updatedAt DateTime
  ipAddress String?
  userAgent String?
  userId    String
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  impersonatedBy String?

  @@unique([token])

model Account {
  id                    String    @id @default(uuid())
  accountId             String
  providerId            String
  userId                String
  user                  User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  accessToken           String?
  refreshToken          String?
  idToken               String?
  accessTokenExpiresAt  DateTime?
  refreshTokenExpiresAt DateTime?
  scope                 String?
  password              String?
  createdAt             DateTime
  updatedAt             DateTime

  @@map("accounts")
}

model Subscription {
  id                   String    @id @default(uuid())
  plan                 String
  referenceId          String
  stripeCustomerId     String?
  stripeSubscriptionId String?
  status               String
  periodStart          DateTime?
  periodEnd            DateTime?
  cancelAtPeriodEnd    Boolean?
  seats                Int?
  trialStart           DateTime?
  trialEnd             DateTime?

  @@map("subscriptions")
}
Originally created by @ayrtonaguiar on GitHub (Apr 2, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Create a new user subscription via Stripe. 2. Wait for the Stripe webhook to be triggered and attempt to update the subscription in the database. 3. Check the server logs for the error. ### Current vs. Expected behavior **Current Behavior:** The webhook fails due to invalid values for periodStart and periodEnd. > 2025-04-02T12:18:50.775Z ERROR [Better Auth]: Stripe webhook failed. Error: > Invalid `db[getModelName(model)].update()` invocation in > C:\Users\ayrto\tibiaprofit\.next\server\chunks\node_modules_better-auth_dist_e7fe42cb._.js:3150:62 > > 3147 } > 3148 const whereClause = convertWhereClause(model, where); > 3149 const transformed = transformInput(update, model, "update"); > → 3150 const result = await db[getModelName(model)].update({ > where: { > id: "wLFRtVgXFAouHdu8QflWJtfLBQpo7dBL" > }, > data: { > plan: "explorer", > stripeSubscriptionId: "sub_1R9QFoGfZ5Enq2zqaMkwILzy", > status: "trialing", > periodStart: new Date("Invalid Date"), > ~~~~~~~~~~~~~~~~~~~~~~~~ > periodEnd: new Date("Invalid Date"), > seats: 1 > } > }) > > Invalid value for argument `periodStart`: Provided Date object is invalid. Expected Date. **Expected Behavior:** The webhook should successfully process the user's subscription update, ensuring periodStart and periodEnd are valid dates. ### What version of Better Auth are you using? 1.2.4 ### Provide environment information ```bash "@better-auth/stripe": "^1.2.5" "@prisma/client": "^6.5.0" "better-auth": "^1.2.4" "stripe": "^18.0.0" ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; import prisma from "./prisma"; import { nextCookies } from "better-auth/next-js"; import { admin, customSession } from "better-auth/plugins"; import Stripe from "stripe"; import { stripe } from "@better-auth/stripe"; const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2025-03-31.basil', typescript: true, }); export const auth = betterAuth({ database: prismaAdapter(prisma, { provider: "postgresql", }), plugins: [ stripe({ stripeClient, stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!, createCustomerOnSignUp: true, subscription: { enabled: true, plans: [ { name: "explorer", priceId: "price_1R9GGmGfZ5Enq2zqo5hHqPSn", freeTrial: { days: 7, }, limits: { characters: 1, }, } ] } }), customSession( async ({ session, user }) => { const userWithActiveCharacter = await prisma.user.findFirst({ where: { id: user.id }, include: { activeCharacter: true, }, }); return { user, session, activeCharacter: userWithActiveCharacter?.activeCharacter }; } ), admin(), nextCookies() ], socialProviders: { google: { clientId: process.env.GOOGLE_CLIENT_ID as string, clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, }, discord: { clientId: process.env.DISCORD_CLIENT_ID as string, clientSecret: process.env.DISCORD_CLIENT_SECRET as string, }, }, user: { additionalFields: { activeCharacterId: { type: "string", required: false, defaultValue: null, }, language: { type: "string", required: false, defaultValue: "en", }, } }, }); ``` ### Additional context **schema.prisma** ``` model User { id String @id @default(uuid()) name String email String emailVerified Boolean image String? language String? role String? banned Boolean? banReason String? banExpires DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt sessions Session[] accounts Account[] stripeCustomerId String? @@unique([email]) @@map("users") } model Session { id String @id @default(uuid()) expiresAt DateTime token String createdAt DateTime updatedAt DateTime ipAddress String? userAgent String? userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) impersonatedBy String? @@unique([token]) model Account { id String @id @default(uuid()) accountId String providerId String userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) accessToken String? refreshToken String? idToken String? accessTokenExpiresAt DateTime? refreshTokenExpiresAt DateTime? scope String? password String? createdAt DateTime updatedAt DateTime @@map("accounts") } model Subscription { id String @id @default(uuid()) plan String referenceId String stripeCustomerId String? stripeSubscriptionId String? status String periodStart DateTime? periodEnd DateTime? cancelAtPeriodEnd Boolean? seats Int? trialStart DateTime? trialEnd DateTime? @@map("subscriptions") } ```
GiteaMirror added the bug label 2026-03-13 08:12:14 -05:00
Author
Owner

@ayrtonaguiar commented on GitHub (Apr 3, 2025):

Update:
The error occurs because, starting from the 2025-03-01.dashboard version of the Stripe API, the current_period_start and current_period_end fields have been deprecated in the subscriptions API. These fields are now available under subscription.items.data[0].

Official Stripe message:

The current_period_start and current_period_end fields have been deprecated for the subscriptions API. Please use the current_period_start and current_period_end fields on the items[] field instead.

Solution:
To temporarily resolve this issue, downgrade the Stripe API version to 17.7.0, where these fields are still available directly in the subscription object.

Steps to Downgrade:

  1. Update the Stripe package in your project:
    npm install stripe@17.7.0
  2. Verify the API version being used by checking the Stripe configuration in your code.
  3. Check the API version configured in your Stripe Dashboard.It should be set to "Acacia"
  4. Restart the server and test the webhook to ensure the subscription updates work correctly.

Alternative Solution:
If staying on the latest Stripe API version, update the code to retrieve current_period_start and current_period_end using:

The issue is in hooks.ts:

periodStart: new Date(subscription.current_period_start * 1000),
periodEnd: new Date(subscription.current_period_end * 1000),

Ensure that subscription.items.data[0] exists and contains the expected data before accessing these fields.

@ayrtonaguiar commented on GitHub (Apr 3, 2025): **Update:** The error occurs because, starting from the 2025-03-01.dashboard version of the Stripe API, the current_period_start and current_period_end fields have been deprecated in the subscriptions API. These fields are now available under subscription.items.data[0]. **Official Stripe message:** > The current_period_start and current_period_end fields have been deprecated for the subscriptions API. Please use the current_period_start and current_period_end fields on the items[] field instead. **Solution:** To temporarily resolve this issue, downgrade the Stripe API version to 17.7.0, where these fields are still available directly in the subscription object. **Steps to Downgrade:** 1. Update the Stripe package in your project: `npm install stripe@17.7.0` 2. Verify the API version being used by checking the Stripe configuration in your code. 3. Check the API version configured in your Stripe Dashboard.It should be set to "Acacia" 4. Restart the server and test the webhook to ensure the subscription updates work correctly. **Alternative Solution:** If staying on the latest Stripe API version, update the code to retrieve current_period_start and current_period_end using: The issue is in [`hooks.ts`](https://github.com/better-auth/better-auth/blob/main/packages/stripe/src/hooks.ts#L42-L43): ```javascript periodStart: new Date(subscription.current_period_start * 1000), periodEnd: new Date(subscription.current_period_end * 1000), Ensure that subscription.items.data[0] exists and contains the expected data before accessing these fields.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#961