[Stripe] How to use created session and forward in headers to make new api call to signup user to free plan upon new customer creation #2035

Closed
opened 2026-03-13 09:22:14 -05:00 by GiteaMirror · 9 comments
Owner

Originally created by @Zefty on GitHub (Sep 28, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

For the onCustomerCreate how can we call auth.api.upgradeSubscription without the UNAUTHORIZED error. This occurs because when the user signs up a session is not yet created so when we try to subscribe the customer to a free plan, it will error out.

I tried to bypass this in onCustomerCreate by manually creating a db session and then setting the session token in the headers for the next request but it doesn't seem to work.

What is the recommended way to achieve this?

plugins: [
    nextCookies(),
    stripe({
      stripeClient,
      stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
      createCustomerOnSignUp: true,
      onCustomerCreate: async ({ stripeCustomer, user }, request) => {
        const session = await request.context.internalAdapter.createSession(
          user.id,
          request,
        );
        console.log(session);
        request.context.setNewSession({
          session,
          user,
        });
        try {
          const data = await auth.api.upgradeSubscription({
            body: {
              plan: "free",
              successUrl: "/dashboard",
              cancelUrl: "/dashboard",
              returnUrl: "/dashboard",
            },
            headers: new Headers({
              cookie: `better-auth.session_token=${session.token}`,
            }),
          });

          console.log("[onCustomerCreate] Success", data);
        } catch (err) {
          console.error("[onCustomerCreate] Error", err);
        }
      },
    

Current vs. Expected behavior

Inside onCustomerCreate i can call auth.api.upgradeSubscription to immediately upgrade user to a free plan

What version of Better Auth are you using?

1.3.8

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 25.0.0: Mon Aug 25 21:12:01 PDT 2025; root:xnu-12377.1.9~3/RELEASE_ARM64_T8132",
    "release": "25.0.0",
    "cpuCount": 10,
    "cpuModel": "Apple M4",
    "totalMemory": "16.00 GB",
    "freeMemory": "0.11 GB"
  },
  "node": {
    "version": "v24.4.1",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.4.2"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "^15.5.3"
    },
    {
      "name": "react",
      "version": "19.1.1"
    }
  ],
  "databases": [
    {
      "name": "pg",
      "version": "^8.16.3"
    },
    {
      "name": "postgres",
      "version": "^3.4.7"
    },
    {
      "name": "drizzle",
      "version": "^0.44.5"
    }
  ],
  "betterAuth": {
    "version": "^1.3.8",
    "config": null
  }
}

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

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
});

Additional context

No response

Originally created by @Zefty on GitHub (Sep 28, 2025). ### Is this suited for github? - [ ] Yes, this is suited for github ### To Reproduce For the `onCustomerCreate` how can we call `auth.api.upgradeSubscription` without the UNAUTHORIZED error. This occurs because when the user signs up a session is not yet created so when we try to subscribe the customer to a free plan, it will error out. I tried to bypass this in onCustomerCreate by manually creating a db session and then setting the session token in the headers for the next request but it doesn't seem to work. What is the recommended way to achieve this? ``` plugins: [ nextCookies(), stripe({ stripeClient, stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!, createCustomerOnSignUp: true, onCustomerCreate: async ({ stripeCustomer, user }, request) => { const session = await request.context.internalAdapter.createSession( user.id, request, ); console.log(session); request.context.setNewSession({ session, user, }); try { const data = await auth.api.upgradeSubscription({ body: { plan: "free", successUrl: "/dashboard", cancelUrl: "/dashboard", returnUrl: "/dashboard", }, headers: new Headers({ cookie: `better-auth.session_token=${session.token}`, }), }); console.log("[onCustomerCreate] Success", data); } catch (err) { console.error("[onCustomerCreate] Error", err); } }, ``` ### Current vs. Expected behavior Inside `onCustomerCreate` i can call `auth.api.upgradeSubscription` to immediately upgrade user to a free plan ### What version of Better Auth are you using? 1.3.8 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 25.0.0: Mon Aug 25 21:12:01 PDT 2025; root:xnu-12377.1.9~3/RELEASE_ARM64_T8132", "release": "25.0.0", "cpuCount": 10, "cpuModel": "Apple M4", "totalMemory": "16.00 GB", "freeMemory": "0.11 GB" }, "node": { "version": "v24.4.1", "env": "development" }, "packageManager": { "name": "npm", "version": "11.4.2" }, "frameworks": [ { "name": "next", "version": "^15.5.3" }, { "name": "react", "version": "19.1.1" } ], "databases": [ { "name": "pg", "version": "^8.16.3" }, { "name": "postgres", "version": "^3.4.7" }, { "name": "drizzle", "version": "^0.44.5" } ], "betterAuth": { "version": "^1.3.8", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context _No response_
GiteaMirror added the enhancement label 2026-03-13 09:22:14 -05:00
Author
Owner

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

To avoid the UNAUTHORIZED error when calling auth.api.upgradeSubscription in onCustomerCreate, you have two recommended options:

1. Use API Key authentication:
If you need to perform privileged actions (like upgrading a subscription) before a session exists, use the API Key plugin. Create an API key for an admin user, then include it in your request headers:

const response = await auth.api.upgradeSubscription({
  body: {
    plan: 'free',
    referenceId: user.id,
    successUrl: '/dashboard',
    cancelUrl: '/pricing',
    disableRedirect: true,
  },
  headers: new Headers({
    'x-api-key': process.env.BETTER_AUTH_SUPER_ADMIN_API_KEY,
  }),
});

This pattern is recommended for backend automation and Stripe webhook flows where no user session is present yet.
Reference

2. If you want to use a session:
When manually creating a session (e.g., with internalAdapter.createSession), you must generate a properly signed and encoded cookie string using authClient.getCookie(session). Do not set the cookie header with just the raw session token, as this will fail validation:

const session = await request.context.internalAdapter.createSession(user.id, request);
const cookieHeader = authClient.getCookie(session);

const data = await auth.api.upgradeSubscription({
  body: { /* ... */ },
  headers: new Headers({
    cookie: cookieHeader,
  }),
});

This ensures the session is recognized by Better Auth for authenticated API calls.
Reference
Reference

Why your current approach fails:
Setting cookie: better-auth.session_token=${session.token} manually does not work because Better Auth expects a signed, URL-encoded cookie—not just the raw token.

Is this suited for GitHub?
Yes, this is a common integration and session management scenario for backend auth flows and Stripe onboarding, and is appropriate for GitHub discussion.

Let me know if you need a code snippet for API key creation or more details on session helpers!

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Sep 28, 2025): <!-- Answer --> To avoid the UNAUTHORIZED error when calling `auth.api.upgradeSubscription` in `onCustomerCreate`, you have two recommended options: **1. Use API Key authentication:** If you need to perform privileged actions (like upgrading a subscription) before a session exists, use the API Key plugin. Create an API key for an admin user, then include it in your request headers: ```js const response = await auth.api.upgradeSubscription({ body: { plan: 'free', referenceId: user.id, successUrl: '/dashboard', cancelUrl: '/pricing', disableRedirect: true, }, headers: new Headers({ 'x-api-key': process.env.BETTER_AUTH_SUPER_ADMIN_API_KEY, }), }); ``` This pattern is recommended for backend automation and Stripe webhook flows where no user session is present yet. [Reference](https://github.com/better-auth/better-auth/issues/4861) **2. If you want to use a session:** When manually creating a session (e.g., with `internalAdapter.createSession`), you must generate a properly signed and encoded cookie string using `authClient.getCookie(session)`. Do not set the cookie header with just the raw session token, as this will fail validation: ```js const session = await request.context.internalAdapter.createSession(user.id, request); const cookieHeader = authClient.getCookie(session); const data = await auth.api.upgradeSubscription({ body: { /* ... */ }, headers: new Headers({ cookie: cookieHeader, }), }); ``` This ensures the session is recognized by Better Auth for authenticated API calls. [Reference](https://github.com/better-auth/better-auth/issues/4517) [Reference](https://github.com/better-auth/better-auth/issues/3892) **Why your current approach fails:** Setting `cookie: better-auth.session_token=${session.token}` manually does not work because Better Auth expects a signed, URL-encoded cookie—not just the raw token. **Is this suited for GitHub?** Yes, this is a common integration and session management scenario for backend auth flows and Stripe onboarding, and is appropriate for GitHub discussion. Let me know if you need a code snippet for API key creation or more details on session helpers! <!-- 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/8706a129-f9ba-4de6-b2dc-dea9e867f6b8?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/8706a129-f9ba-4de6-b2dc-dea9e867f6b8?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/8706a129-f9ba-4de6-b2dc-dea9e867f6b8?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/8706a129-f9ba-4de6-b2dc-dea9e867f6b8?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/8706a129-f9ba-4de6-b2dc-dea9e867f6b8?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/8706a129-f9ba-4de6-b2dc-dea9e867f6b8?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/8706a129-f9ba-4de6-b2dc-dea9e867f6b8?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/4954)
Author
Owner

@Zefty commented on GitHub (Sep 28, 2025):

Can you call const cookieHeader = authClient.getCookie(session); server side? @dosu

@Zefty commented on GitHub (Sep 28, 2025): Can you call `const cookieHeader = authClient.getCookie(session);` server side? [@dosu](https://go.dosu.dev/dosubot)
Author
Owner

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

Yes, you can call authClient.getCookie(session) server-side. It's designed to take a session object and return a properly signed and encoded cookie string, which you can use in HTTP headers for backend API calls or automation flows. This is the recommended way to ensure Better Auth recognizes the session for authenticated requests—just passing the raw session token won't work because the cookie needs to be signed and encoded by the helper function. This approach is safe and intended for server-side use cases like webhooks or backend integrations1 2 .

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Sep 28, 2025): <!-- Answer --> Yes, you can call `authClient.getCookie(session)` server-side. It's designed to take a session object and return a properly signed and encoded cookie string, which you can use in HTTP headers for backend API calls or automation flows. This is the recommended way to ensure Better Auth recognizes the session for authenticated requests—just passing the raw session token won't work because the cookie needs to be signed and encoded by the helper function. This approach is safe and intended for server-side use cases like webhooks or backend integrations[^1][^2]. [^1]: [Discussion on correct cookie handling](https://github.com/better-auth/better-auth/issues/4517) [^2]: [Example and explanation](https://github.com/better-auth/better-auth/issues/3892) <!-- 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/1c4c4ab0-834e-4698-b4ae-d30dc415c40a?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/1c4c4ab0-834e-4698-b4ae-d30dc415c40a?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/1c4c4ab0-834e-4698-b4ae-d30dc415c40a?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/1c4c4ab0-834e-4698-b4ae-d30dc415c40a?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/1c4c4ab0-834e-4698-b4ae-d30dc415c40a?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/1c4c4ab0-834e-4698-b4ae-d30dc415c40a?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/1c4c4ab0-834e-4698-b4ae-d30dc415c40a?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/4954)
Author
Owner

@Zefty commented on GitHub (Sep 28, 2025):

This doesn't seem to quite work @dosu

onCustomerCreate] Error [Error [APIError]: ] {
  status: 'UNAUTHORIZED',
  body: undefined,
  headers: {},
  statusCode: 401
}
[TypeError: Failed to parse URL from /api/auth/get-cookie] {
  [cause]: [TypeError: Invalid URL] {
    code: 'ERR_INVALID_URL',
    input: '/api/auth/get-cookie'
  }
}
 ⨯ unhandledRejection: [TypeError: Failed to parse URL from /api/auth/get-cookie] {
  [cause]: [TypeError: Invalid URL] {
    code: 'ERR_INVALID_URL',
    input: '/api/auth/get-cookie'
  }
}
 ⨯ unhandledRejection:  [TypeError: Failed to parse URL from /api/auth/get-cookie] {
  [cause]: [TypeError: Invalid URL] {
    code: 'ERR_INVALID_URL',
    input: '/api/auth/get-cookie'
  }
}
@Zefty commented on GitHub (Sep 28, 2025): This doesn't seem to quite work [@dosu](https://go.dosu.dev/dosubot) ``` onCustomerCreate] Error [Error [APIError]: ] { status: 'UNAUTHORIZED', body: undefined, headers: {}, statusCode: 401 } [TypeError: Failed to parse URL from /api/auth/get-cookie] { [cause]: [TypeError: Invalid URL] { code: 'ERR_INVALID_URL', input: '/api/auth/get-cookie' } } ⨯ unhandledRejection: [TypeError: Failed to parse URL from /api/auth/get-cookie] { [cause]: [TypeError: Invalid URL] { code: 'ERR_INVALID_URL', input: '/api/auth/get-cookie' } } ⨯ unhandledRejection: [TypeError: Failed to parse URL from /api/auth/get-cookie] { [cause]: [TypeError: Invalid URL] { code: 'ERR_INVALID_URL', input: '/api/auth/get-cookie' } } ```
Author
Owner

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

The error is happening because authClient.getCookie is a local helper function, not an API endpoint—calling /api/auth/get-cookie or using it as a URL will always fail with a "Invalid URL" error. You should import and call authClient.getCookie(session) directly in your server-side code, passing a valid session object, and use the returned cookie string in your headers. If you're on a recent Better Auth version, note that getCookie may not exist as a public API; the recommended way to get a valid authentication cookie is to use the sign-in API with returnHeaders: true and extract the Set-Cookie header from the response—this is how the codebase and tests handle authenticated requests now [source].

The 401 UNAUTHORIZED error is likely because the session isn't being recognized or the authentication context is missing. For user-specific subscription upgrades, Better Auth expects the request to come from an authenticated session for that user. Server-side hooks like onCustomerCreate can't impersonate arbitrary users for onboarding actions; the recommended pattern is to perform the upgrade client-side after the user is authenticated [source].

If you need to automate backend actions, use an API key for admin-level operations, but this will act as the API key owner, not the new user. For onboarding flows, redirect the user client-side after signup and call the upgrade API from there.

Troubleshooting checklist:

  • Make sure you're not calling /api/auth/get-cookie as a URL.
  • If you need a valid cookie, use the sign-in API and extract Set-Cookie from the response.
  • Ensure your Stripe plugin config has subscription: { enabled: true }.
  • Double-check that your auth instance is exported statically for plugin endpoint registration if you're in a serverless environment [source].

Let me know if you want a minimal code example for the sign-in/cookie flow or client-side onboarding!

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Sep 28, 2025): <!-- Answer --> The error is happening because `authClient.getCookie` is a local helper function, not an API endpoint—calling `/api/auth/get-cookie` or using it as a URL will always fail with a "Invalid URL" error. You should import and call `authClient.getCookie(session)` directly in your server-side code, passing a valid session object, and use the returned cookie string in your headers. If you're on a recent Better Auth version, note that `getCookie` may not exist as a public API; the recommended way to get a valid authentication cookie is to use the sign-in API with `returnHeaders: true` and extract the `Set-Cookie` header from the response—this is how the codebase and tests handle authenticated requests now [[source](https://github.com/better-auth/better-auth/issues/4940)]. The 401 UNAUTHORIZED error is likely because the session isn't being recognized or the authentication context is missing. For user-specific subscription upgrades, Better Auth expects the request to come from an authenticated session for that user. Server-side hooks like `onCustomerCreate` can't impersonate arbitrary users for onboarding actions; the recommended pattern is to perform the upgrade client-side after the user is authenticated [[source](https://github.com/better-auth/better-auth/issues/4861)]. If you need to automate backend actions, use an API key for admin-level operations, but this will act as the API key owner, not the new user. For onboarding flows, redirect the user client-side after signup and call the upgrade API from there. Troubleshooting checklist: - Make sure you're not calling `/api/auth/get-cookie` as a URL. - If you need a valid cookie, use the sign-in API and extract `Set-Cookie` from the response. - Ensure your Stripe plugin config has `subscription: { enabled: true }`. - Double-check that your auth instance is exported statically for plugin endpoint registration if you're in a serverless environment [[source](https://github.com/better-auth/better-auth/issues/4056)]. Let me know if you want a minimal code example for the sign-in/cookie flow or client-side onboarding! <!-- 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/1127f315-0ee2-4a6c-a881-21a101987875?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/1127f315-0ee2-4a6c-a881-21a101987875?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/1127f315-0ee2-4a6c-a881-21a101987875?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/1127f315-0ee2-4a6c-a881-21a101987875?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/1127f315-0ee2-4a6c-a881-21a101987875?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/1127f315-0ee2-4a6c-a881-21a101987875?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/1127f315-0ee2-4a6c-a881-21a101987875?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/4954)
Author
Owner

@Zefty commented on GitHub (Sep 29, 2025):

Why doesnt the follow code work? I have signed and set the cookies as you have described. @dosu

const tempSession = await request.context.internalAdapter.createSession(
          user.id,
          request,
        );

        console.log({ tempSession });

        const tempSessionCookie =
          request.context.createAuthCookie("temp_session");

        console.log({ tempSessionCookie });

        const signedCookie = await request.setSignedCookie(
          tempSessionCookie.name,
          tempSession.token,
          request.context.secret,
        );

        console.log({ signedCookie });

        try {
          const data = await auth.api.upgradeSubscription({
            body: {
              plan: "free",
              successUrl: "/dashboard",
              cancelUrl: "/dashboard",
              returnUrl: "/dashboard",
            },
            headers: new Headers({
              cookie: signedCookie,
            }),
          });
@Zefty commented on GitHub (Sep 29, 2025): Why doesnt the follow code work? I have signed and set the cookies as you have described. [@dosu](https://go.dosu.dev/dosubot) ``` const tempSession = await request.context.internalAdapter.createSession( user.id, request, ); console.log({ tempSession }); const tempSessionCookie = request.context.createAuthCookie("temp_session"); console.log({ tempSessionCookie }); const signedCookie = await request.setSignedCookie( tempSessionCookie.name, tempSession.token, request.context.secret, ); console.log({ signedCookie }); try { const data = await auth.api.upgradeSubscription({ body: { plan: "free", successUrl: "/dashboard", cancelUrl: "/dashboard", returnUrl: "/dashboard", }, headers: new Headers({ cookie: signedCookie, }), }); ```
Author
Owner

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

Your code doesn't work because manually creating and signing session cookies with request.setSignedCookie doesn't fully replicate Better Auth's internal cookie logic. The session token alone isn't enough—Better Auth expects a signed, URL-encoded cookie with specific structure and metadata, which is tightly controlled and sensitive to recent changes (like the dontRememberMe flag and cookie expiration) [source]. Manual signing often misses these details, so the API treats your request as unauthorized.

For backend automation (like Stripe webhooks), the recommended approach is to use the API Key plugin: create an API key for an admin user and include it in the x-api-key header when calling privileged endpoints. This avoids session issues and is designed for server-side flows [source]:

const response = await auth.api.upgradeSubscription({
  body: {
    plan: 'free',
    referenceId: user.id,
    successUrl: '/dashboard',
    cancelUrl: '/dashboard',
    disableRedirect: true,
  },
  headers: new Headers({
    'x-api-key': process.env.BETTER_AUTH_SUPER_ADMIN_API_KEY,
  }),
});

If you need to act as a specific user, use the sign-in API with asResponse: true and extract the Set-Cookie header for subsequent requests [source]. Manual cookie signing is not supported and will likely fail due to these internal checks.

For onboarding, always redirect the user client-side after authentication and call the upgrade API from there to ensure the subscription is tied to the correct user session [source].

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Sep 29, 2025): <!-- Answer --> Your code doesn't work because manually creating and signing session cookies with `request.setSignedCookie` doesn't fully replicate Better Auth's internal cookie logic. The session token alone isn't enough—Better Auth expects a signed, URL-encoded cookie with specific structure and metadata, which is tightly controlled and sensitive to recent changes (like the `dontRememberMe` flag and cookie expiration) [[source](https://github.com/better-auth/better-auth/pull/4558)]. Manual signing often misses these details, so the API treats your request as unauthorized. For backend automation (like Stripe webhooks), the recommended approach is to use the API Key plugin: create an API key for an admin user and include it in the `x-api-key` header when calling privileged endpoints. This avoids session issues and is designed for server-side flows [[source](https://github.com/better-auth/better-auth/issues/4861)]: ```js const response = await auth.api.upgradeSubscription({ body: { plan: 'free', referenceId: user.id, successUrl: '/dashboard', cancelUrl: '/dashboard', disableRedirect: true, }, headers: new Headers({ 'x-api-key': process.env.BETTER_AUTH_SUPER_ADMIN_API_KEY, }), }); ``` If you need to act as a specific user, use the sign-in API with `asResponse: true` and extract the `Set-Cookie` header for subsequent requests [[source](https://github.com/better-auth/better-auth/issues/4945)]. Manual cookie signing is not supported and will likely fail due to these internal checks. For onboarding, always redirect the user client-side after authentication and call the upgrade API from there to ensure the subscription is tied to the correct user session [[source](https://github.com/better-auth/better-auth/issues/4861)]. <!-- 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/217eef3e-c081-416f-b47f-7b829bd772d7?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/217eef3e-c081-416f-b47f-7b829bd772d7?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/217eef3e-c081-416f-b47f-7b829bd772d7?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/217eef3e-c081-416f-b47f-7b829bd772d7?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/217eef3e-c081-416f-b47f-7b829bd772d7?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/217eef3e-c081-416f-b47f-7b829bd772d7?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/217eef3e-c081-416f-b47f-7b829bd772d7?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/4954)
Author
Owner

@bytaesu commented on GitHub (Nov 7, 2025):

Hello @Zefty,

Could you try setting it up like this?

I tested it in a real app, and the subscription was created successfully. While we could add an option for it, this case seems well-suited for implementation through a hook. Please leave a comment if you think this approach isn’t appropriate.

createCustomerOnSignUp: true,
onCustomerCreate: async ({ stripeCustomer, user }, ctx) => {
	const subscription = await stripeClient.subscriptions.create({
		customer: stripeCustomer.id,
		items: [{ price: YOUR_FREE_PRICE_ID }],
	});
	await ctx.context.adapter.create({
		model: "subscription",
		data: {
			referenceId: user.id,
			stripeCustomerId: stripeCustomer.id,
			stripeSubscriptionId: subscription.id,
			status: "active",
			plan: "free",
		},
	});
},
@bytaesu commented on GitHub (Nov 7, 2025): Hello @Zefty, Could you try setting it up like this? I tested it in a real app, and the subscription was created successfully. While we could add an option for it, this case seems well-suited for implementation through a hook. Please leave a comment if you think this approach isn’t appropriate. ```ts createCustomerOnSignUp: true, onCustomerCreate: async ({ stripeCustomer, user }, ctx) => { const subscription = await stripeClient.subscriptions.create({ customer: stripeCustomer.id, items: [{ price: YOUR_FREE_PRICE_ID }], }); await ctx.context.adapter.create({ model: "subscription", data: { referenceId: user.id, stripeCustomerId: stripeCustomer.id, stripeSubscriptionId: subscription.id, status: "active", plan: "free", }, }); }, ```
Author
Owner

@bytaesu commented on GitHub (Nov 20, 2025):

Closing this issue. Please comment if the problem persists.

@bytaesu commented on GitHub (Nov 20, 2025): Closing this issue. Please comment if the problem persists.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2035