--- title: createCustomerAccountClient description: >- The `createCustomerAccountClient` function creates a GraphQL client for querying the [Customer Account API](https://shopify.dev/docs/api/customer). It also provides methods to authenticate and check if the user is logged in. api_version: 2025-07 api_name: hydrogen source_url: html: >- https://shopify.dev/docs/api/hydrogen/latest/utilities/createcustomeraccountclient md: >- https://shopify.dev/docs/api/hydrogen/latest/utilities/createcustomeraccountclient.md --- # create​Customer​Account​Client The `createCustomerAccountClient` function creates a GraphQL client for querying the [Customer Account API](https://shopify.dev/docs/api/customer). It also provides methods to authenticate and check if the user is logged in. ## createCustomerAccountClient(options) * customerAccountId string required Unique UUID prefixed with `shp_` associated with the application, this should be visible in the customer account api settings in the Hydrogen admin channel. Mock.shop doesn't automatically supply customerAccountId. Use `npx shopify hydrogen env pull` to link your store credentials. * request CrossRuntimeRequest required The object for the current Request. It should be provided by your platform. * session HydrogenSession required The client requires a session to persist the auth and refresh token. By default Hydrogen ships with cookie session storage, but you can use [another session storage](https://remix.run/docs/en/main/utils/sessions) implementation. * shopId string required The shop id. Mock.shop doesn't automatically supply shopId. Use `npx shopify hydrogen env pull` to link your store credentials * authorizePath string The oauth authorize path. Defaults to `/account/authorize`. * authUrl string This is the route in your app that authorizes the customer after logging in. Make sure to call `customer.authorize()` within the loader on this route. It defaults to `/account/authorize`. * customAuthStatusHandler () => DataFunctionValue Use this method to overwrite the default logged-out redirect behavior. The default handler [throws a redirect](https://remix.run/docs/en/main/utils/redirect#:~:text=!session) to `/account/login` with current path as `return_to` query param. * customerApiVersion string Override the version of the API * defaultRedirectPath string The path to redirect to after login. Defaults to `/account`. * language LanguageCode Localization data. * logErrors boolean | ((error?: Error) => boolean) Whether it should print GraphQL errors automatically. Defaults to true * loginPath string The path to login. Defaults to `/account/login`. * unstableB2b boolean Deprecated. `unstableB2b` is now stable. Please remove. * waitUntil WaitUntil The waitUntil function is used to keep the current request/response lifecycle alive even after a response has been sent. It should be provided by your platform. ### DataFunctionValue ```ts Response | NonNullable | null ``` ### CrossRuntimeRequest * headers ```ts { [key: string]: any; get?: (key: string) => string; } ``` * method ```ts string ``` * url ```ts string ``` ```ts { url?: string; method?: string; headers: { get?: (key: string) => string | null | undefined; [key: string]: any; }; } ``` ## Returns * authorize () => Promise\ On successful login, the customer redirects back to your app. This function validates the OAuth response and exchanges the authorization code for an access token and refresh token. It also persists the tokens on your session. This function should be called and returned from the Remix loader configured as the redirect URI within the Customer Account API settings in admin. * getAccessToken () => Promise\ Returns CustomerAccessToken if the customer is logged in. It also run a expiry check and does a token refresh if needed. * getApiUrl () => string Creates the fully-qualified URL to your store's GraphQL endpoint. * handleAuthStatus () => void | DataFunctionValue Check for a not logged in customer and redirect customer to login page. The redirect can be overwritten with `customAuthStatusHandler` option. * isLoggedIn () => Promise\ Returns if the customer is logged in. It also checks if the access token is expired and refreshes it if needed. * login (options?: LoginOptions) => Promise\ Start the OAuth login flow. This function should be called and returned from a Remix action. It redirects the customer to a Shopify login domain. It also defined the final path the customer lands on at the end of the oAuth flow with the value of the `return_to` query param. (This is automatically setup unless `customAuthStatusHandler` option is in use) * logout (options?: LogoutOptions) => Promise\ Logout the customer by clearing the session and redirecting to the login domain. It should be called and returned from a Remix action. The path app should redirect to after logout can be setup in Customer Account API settings in admin. * mutate \(mutation: string, options: CustomerAccountQueryOptionsForDocs) => Promise\ Execute a GraphQL mutation against the Customer Account API. This method execute `handleAuthStatus()` ahead of mutation. * query \(query: string, options: CustomerAccountQueryOptionsForDocs) => Promise\ Execute a GraphQL query against the Customer Account API. This method execute `handleAuthStatus()` ahead of query. ### DataFunctionValue ```ts Response | NonNullable | null ``` ### LoginOptions * countryCode ```ts CountryCode ``` * uiLocales ```ts LanguageCode ``` ```ts { uiLocales?: LanguageCode; countryCode?: CountryCode; } ``` ### LogoutOptions * headers Add custom headers to the logout redirect. ```ts HeadersInit ``` * keepSession If true, custom data in the session will not be cleared on logout. ```ts boolean ``` * postLogoutRedirectUri The url to redirect customer to after logout, should be a relative URL. This url will need to included in Customer Account API's application setup for logout URI. The default value is current app origin, which is automatically setup in admin when using \`--customer-account-push\` flag with dev. ```ts string ``` ```ts { /** The url to redirect customer to after logout, should be a relative URL. This url will need to included in Customer Account API's application setup for logout URI. The default value is current app origin, which is automatically setup in admin when using `--customer-account-push` flag with dev. */ postLogoutRedirectUri?: string; /** Add custom headers to the logout redirect. */ headers?: HeadersInit; /** If true, custom data in the session will not be cleared on logout. */ keepSession?: boolean; } ``` ### CustomerAccountQueryOptionsForDocs * variables The variables for the GraphQL statement. ```ts Record ``` ```ts { /** The variables for the GraphQL statement. */ variables?: Record; } ``` ### Examples * #### Example code ##### Description I am the default example ##### JavaScript ```jsx import {createCustomerAccountClient} from '@shopify/hydrogen'; // @ts-expect-error import * as reactRouterBuild from 'virtual:react-router/server-build'; import { createRequestHandler, createCookieSessionStorage, } from '@shopify/remix-oxygen'; export default { async fetch(request, env, executionContext) { const session = await AppSession.init(request, [env.SESSION_SECRET]); /* Create a Customer API client with your credentials and options */ const customerAccount = createCustomerAccountClient({ /* Runtime utility in serverless environments */ waitUntil: (p) => executionContext.waitUntil(p), /* Public Customer Account API token for your store */ customerAccountId: env.PUBLIC_CUSTOMER_ACCOUNT_ID, /* Shop Id */ shopId: env.SHOP_ID, request, session, }); const handleRequest = createRequestHandler({ build: reactRouterBuild, mode: process.env.NODE_ENV, /* Inject the customer account client in the Remix context */ getLoadContext: () => ({customerAccount}), }); const response = await handleRequest(request); if (session.isPending) { response.headers.set('Set-Cookie', await session.commit()); } return response; }, }; class AppSession { isPending = false; static async init(request, secrets) { const storage = createCookieSessionStorage({ cookie: { name: 'session', httpOnly: true, path: '/', sameSite: 'lax', secrets, }, }); const session = await storage.getSession(request.headers.get('Cookie')); return new this(storage, session); } get(key) { return this.session.get(key); } destroy() { return this.sessionStorage.destroySession(this.session); } flash(key, value) { this.session.flash(key, value); } unset(key) { this.isPending = true; this.session.unset(key); } set(key, value) { this.isPending = true; this.session.set(key, value); } commit() { this.isPending = false; return this.sessionStorage.commitSession(this.session); } } ``` ##### TypeScript ```tsx import { createCustomerAccountClient, type HydrogenSession, } from '@shopify/hydrogen'; // @ts-expect-error import * as reactRouterBuild from 'virtual:react-router/server-build'; import { createRequestHandler, createCookieSessionStorage, type SessionStorage, type Session, } from '@shopify/remix-oxygen'; export default { async fetch( request: Request, env: Record, executionContext: ExecutionContext, ) { const session = await AppSession.init(request, [env.SESSION_SECRET]); /* Create a Customer API client with your credentials and options */ const customerAccount = createCustomerAccountClient({ /* Runtime utility in serverless environments */ waitUntil: (p) => executionContext.waitUntil(p), /* Public Customer Account API client ID for your store */ customerAccountId: env.PUBLIC_CUSTOMER_ACCOUNT_ID, /* Shop Id */ shopId: env.SHOP_ID, request, session, }); const handleRequest = createRequestHandler({ build: reactRouterBuild, mode: process.env.NODE_ENV, /* Inject the customer account client in the Remix context */ getLoadContext: () => ({customerAccount}), }); const response = await handleRequest(request); if (session.isPending) { response.headers.set('Set-Cookie', await session.commit()); } return response; }, }; class AppSession implements HydrogenSession { public isPending = false; constructor( private sessionStorage: SessionStorage, private session: Session, ) {} static async init(request: Request, secrets: string[]) { const storage = createCookieSessionStorage({ cookie: { name: 'session', httpOnly: true, path: '/', sameSite: 'lax', secrets, }, }); const session = await storage.getSession(request.headers.get('Cookie')); return new this(storage, session); } get(key: string) { return this.session.get(key); } destroy() { return this.sessionStorage.destroySession(this.session); } flash(key: string, value: any) { this.session.flash(key, value); } unset(key: string) { this.isPending = true; this.session.unset(key); } set(key: string, value: any) { this.isPending = true; this.session.set(key, value); } commit() { this.isPending = false; return this.sessionStorage.commitSession(this.session); } } ``` ## Examples Examples of how to opt out of default logged-out redirect ### Customized logged-out behavior for the entire application ### Examples * #### Example ##### Description Throw error instead of redirect ##### JavaScript ```jsx import {createCustomerAccountClient} from '@shopify/hydrogen'; // @ts-expect-error import * as reactRouterBuild from 'virtual:react-router/server-build'; import { createRequestHandler, createCookieSessionStorage, } from '@shopify/remix-oxygen'; // In server.ts export default { async fetch(request, env, executionContext) { const session = await AppSession.init(request, [env.SESSION_SECRET]); function customAuthStatusHandler() { return new Response('Customer is not login', { status: 401, }); } /* Create a Customer API client with your credentials and options */ const customerAccount = createCustomerAccountClient({ /* Runtime utility in serverless environments */ waitUntil: (p) => executionContext.waitUntil(p), /* Public Customer Account API client ID for your store */ customerAccountId: env.PUBLIC_CUSTOMER_ACCOUNT_ID, /* Shop Id */ shopId: env.SHOP_ID, request, session, customAuthStatusHandler, }); const handleRequest = createRequestHandler({ build: reactRouterBuild, mode: process.env.NODE_ENV, /* Inject the customer account client in the Remix context */ getLoadContext: () => ({customerAccount}), }); const response = await handleRequest(request); if (session.isPending) { response.headers.set('Set-Cookie', await session.commit()); } return response; }, }; class AppSession { isPending = false; static async init(request, secrets) { const storage = createCookieSessionStorage({ cookie: { name: 'session', httpOnly: true, path: '/', sameSite: 'lax', secrets, }, }); const session = await storage.getSession(request.headers.get('Cookie')); return new this(storage, session); } get(key) { return this.session.get(key); } destroy() { return this.sessionStorage.destroySession(this.session); } flash(key, value) { this.session.flash(key, value); } unset(key) { this.isPending = true; this.session.unset(key); } set(key, value) { this.isPending = true; this.session.set(key, value); } commit() { this.isPending = false; return this.sessionStorage.commitSession(this.session); } } ///////////////////////////////// // In a route import { useLoaderData, useRouteError, isRouteErrorResponse, useLocation, } from 'react-router'; export async function loader({context}) { const {data} = await context.customerAccount.query(`#graphql query getCustomer { customer { firstName lastName } } `); return {customer: data.customer}; } export function ErrorBoundary() { const error = useRouteError(); const location = useLocation(); if (isRouteErrorResponse(error)) { if (error.status == 401) { return ( Login ); } } } // this should be an default export export function Route() { const {customer} = useLoaderData(); return (
{customer ? ( <>
Welcome {customer.firstName} {customer.lastName}
) : null}
); } ``` ##### TypeScript ```tsx // Example: Custom session implementation in server.ts // This shows how to use a custom session class with Hydrogen import {createHydrogenContext, type HydrogenSession} from '@shopify/hydrogen'; import {createRequestHandler} from '@shopify/remix-oxygen'; import { createCookieSessionStorage, type SessionStorage, type Session, } from '@shopify/remix-oxygen'; // In server.ts export default { async fetch(request: Request, env: Env, executionContext: ExecutionContext) { // Example of using a custom session implementation const session = await AppSession.init(request, [env.SESSION_SECRET]); // Create the Hydrogen context with all the standard services const hydrogenContext = createHydrogenContext({ env, request, cache: {} as Cache, // Use your cache implementation waitUntil: (p) => executionContext.waitUntil(p), session, // Your custom session implementation must satisfy HydrogenSession interface // Add other options as needed }); const handleRequest = createRequestHandler({ // @ts-expect-error build: await import('virtual:react-router/server-build'), mode: process.env.NODE_ENV, getLoadContext: () => hydrogenContext, }); const response = await handleRequest(request); if (hydrogenContext.session.isPending) { response.headers.set( 'Set-Cookie', await hydrogenContext.session.commit(), ); } return response; }, }; class AppSession implements HydrogenSession { public isPending = false; constructor( private sessionStorage: SessionStorage, private session: Session, ) {} static async init(request: Request, secrets: string[]) { const storage = createCookieSessionStorage({ cookie: { name: 'session', httpOnly: true, path: '/', sameSite: 'lax', secrets, }, }); const session = await storage.getSession(request.headers.get('Cookie')); return new this(storage, session); } get(key: string) { return this.session.get(key); } destroy() { return this.sessionStorage.destroySession(this.session); } flash(key: string, value: any) { this.session.flash(key, value); } unset(key: string) { this.isPending = true; this.session.unset(key); } set(key: string, value: any) { this.isPending = true; this.session.set(key, value); } commit() { this.isPending = false; return this.sessionStorage.commitSession(this.session); } } // In env.d.ts // Note: Hydrogen now provides default AppLoadContext augmentation // If you need to extend it with additional properties, you can do: // declare module 'react-router' { // interface AppLoadContext { // // Add your custom properties here // } // } ///////////////////////////////// // In a route (e.g., app/routes/account.tsx) import { useLoaderData, useRouteError, isRouteErrorResponse, useLocation, } from 'react-router'; // Import the Route namespace from the generated types // Note: Replace 'account' with your actual route name // import type {Route} from './+types/account'; // Using the Route.LoaderArgs type from generated types // export async function loader({context}: Route.LoaderArgs) { // For this example, we'll show the pattern with proper typing import type {LoaderFunctionArgs} from 'react-router'; import type {CustomerAccount} from '@shopify/hydrogen'; export async function loader({context}: LoaderFunctionArgs) { const {data} = await context.customerAccount.query<{ customer: {firstName: string; lastName: string}; }>(`#graphql query getCustomer { customer { firstName lastName } } `); return {customer: data.customer}; } export function ErrorBoundary() { const error = useRouteError(); const location = useLocation(); if (isRouteErrorResponse(error)) { if (error.status == 401) { return ( Login ); } } } // this should be an default export export function Route() { const {customer} = useLoaderData(); return (
{customer ? ( <>
Welcome {customer.firstName} {customer.lastName}
) : null}
); } ``` ### Opt out of logged-out behavior for a single route ### Examples * #### Example ##### Description Handle logged-out ahead of query ##### JavaScript ```jsx import {createCustomerAccountClient} from '@shopify/hydrogen'; // @ts-expect-error import * as reactRouterBuild from 'virtual:react-router/server-build'; import { createRequestHandler, createCookieSessionStorage, } from '@shopify/remix-oxygen'; // In server.ts export default { async fetch(request, env, executionContext) { const session = await AppSession.init(request, [env.SESSION_SECRET]); function customAuthStatusHandler() { return new Response('Customer is not login', { status: 401, }); } /* Create a Customer API client with your credentials and options */ const customerAccount = createCustomerAccountClient({ /* Runtime utility in serverless environments */ waitUntil: (p) => executionContext.waitUntil(p), /* Public Customer Account API client ID for your store */ customerAccountId: env.PUBLIC_CUSTOMER_ACCOUNT_ID, /* Shop Id */ shopId: env.SHOP_ID, request, session, customAuthStatusHandler, }); const handleRequest = createRequestHandler({ build: reactRouterBuild, mode: process.env.NODE_ENV, /* Inject the customer account client in the Remix context */ getLoadContext: () => ({customerAccount}), }); const response = await handleRequest(request); if (session.isPending) { response.headers.set('Set-Cookie', await session.commit()); } return response; }, }; class AppSession { isPending = false; static async init(request, secrets) { const storage = createCookieSessionStorage({ cookie: { name: 'session', httpOnly: true, path: '/', sameSite: 'lax', secrets, }, }); const session = await storage.getSession(request.headers.get('Cookie')); return new this(storage, session); } get(key) { return this.session.get(key); } destroy() { return this.sessionStorage.destroySession(this.session); } flash(key, value) { this.session.flash(key, value); } unset(key) { this.isPending = true; this.session.unset(key); } set(key, value) { this.isPending = true; this.session.set(key, value); } commit() { this.isPending = false; return this.sessionStorage.commitSession(this.session); } } ///////////////////////////////// // In a route import { useLoaderData, useRouteError, isRouteErrorResponse, useLocation, } from 'react-router'; export async function loader({context}) { if (!(await context.customerAccount.isLoggedIn())) { throw new Response('Customer is not login', { status: 401, }); } const {data} = await context.customerAccount.query( `#graphql query getCustomer { customer { firstName lastName } } `, ); return {customer: data.customer}; } export function ErrorBoundary() { const error = useRouteError(); const location = useLocation(); if (isRouteErrorResponse(error)) { if (error.status == 401) { return ( Login ); } } } // this should be an default export export function Route() { const {customer} = useLoaderData(); return (
{customer ? ( <>
Welcome {customer.firstName} {customer.lastName}
) : null}
); } ``` ##### TypeScript ```tsx import { createCustomerAccountClient, type HydrogenSession, } from '@shopify/hydrogen'; // @ts-expect-error import * as reactRouterBuild from 'virtual:react-router/server-build'; import { createRequestHandler, createCookieSessionStorage, type SessionStorage, type Session, } from '@shopify/remix-oxygen'; // In server.ts export default { async fetch( request: Request, env: Record, executionContext: ExecutionContext, ) { const session = await AppSession.init(request, [env.SESSION_SECRET]); function customAuthStatusHandler() { return new Response('Customer is not login', { status: 401, }); } /* Create a Customer API client with your credentials and options */ const customerAccount = createCustomerAccountClient({ /* Runtime utility in serverless environments */ waitUntil: (p) => executionContext.waitUntil(p), /* Public Customer Account API client ID for your store */ customerAccountId: env.PUBLIC_CUSTOMER_ACCOUNT_ID, /* Shop Id */ shopId: env.SHOP_ID, request, session, customAuthStatusHandler, }); const handleRequest = createRequestHandler({ build: reactRouterBuild, mode: process.env.NODE_ENV, /* Inject the customer account client in the Remix context */ getLoadContext: () => ({customerAccount}), }); const response = await handleRequest(request); if (session.isPending) { response.headers.set('Set-Cookie', await session.commit()); } return response; }, }; class AppSession implements HydrogenSession { public isPending = false; constructor( private sessionStorage: SessionStorage, private session: Session, ) {} static async init(request: Request, secrets: string[]) { const storage = createCookieSessionStorage({ cookie: { name: 'session', httpOnly: true, path: '/', sameSite: 'lax', secrets, }, }); const session = await storage.getSession(request.headers.get('Cookie')); return new this(storage, session); } get(key: string) { return this.session.get(key); } destroy() { return this.sessionStorage.destroySession(this.session); } flash(key: string, value: any) { this.session.flash(key, value); } unset(key: string) { this.isPending = true; this.session.unset(key); } set(key: string, value: any) { this.isPending = true; this.session.set(key, value); } commit() { this.isPending = false; return this.sessionStorage.commitSession(this.session); } } ///////////////////////////////// // In a route import { useLoaderData, useRouteError, isRouteErrorResponse, useLocation, } from 'react-router'; import {type LoaderFunctionArgs} from '@shopify/remix-oxygen'; export async function loader({context}: LoaderFunctionArgs) { if (!(await context.customerAccount.isLoggedIn())) { throw new Response('Customer is not login', { status: 401, }); } const {data} = await context.customerAccount.query( `#graphql query getCustomer { customer { firstName lastName } } `, ); return {customer: data.customer}; } export function ErrorBoundary() { const error = useRouteError(); const location = useLocation(); if (isRouteErrorResponse(error)) { if (error.status == 401) { return ( Login ); } } } // this should be an default export export function Route() { const {customer} = useLoaderData(); return (
{customer ? ( <>
Welcome {customer.firstName} {customer.lastName}
) : null}
); } ``` ## Related [- createStorefrontClient](https://shopify.dev/docs/api/hydrogen/utilities/createstorefrontclient)