# createCustomerClient > Caution: > This component is in an unstable pre-release state and may have breaking changes in a future release. The `createCustomerClient` 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. See an end to end [example on using the Customer Account API client](https://github.com/Shopify/hydrogen/tree/main/examples/customer-api). ### Example code ```jsx import {createCustomerClient__unstable} from '@shopify/hydrogen'; import * as remixBuild from '@remix-run/dev/server-build'; import { createRequestHandler, createCookieSessionStorage, } from '@shopify/remix-oxygen'; export default { async fetch(request, env, executionContext) { const session = await HydrogenSession.init(request, [env.SESSION_SECRET]); /* Create a Customer API client with your credentials and options */ const customer = createCustomerClient__unstable({ /* Runtime utility in serverless environments */ waitUntil: (p) => executionContext.waitUntil(p), /* Public Customer Account API token for your store */ customerAccountId: env.PUBLIC_CUSTOMER_ACCOUNT_ID, /* Public account URL for your store */ customerAccountUrl: env.PUBLIC_CUSTOMER_ACCOUNT_URL, request, session, }); const handleRequest = createRequestHandler({ build: remixBuild, mode: process.env.NODE_ENV, /* Inject the customer account client in the Remix context */ getLoadContext: () => ({customer}), }); return handleRequest(request); }, }; class HydrogenSession { 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.session.unset(key); } set(key, value) { this.session.set(key, value); } commit() { return this.sessionStorage.commitSession(this.session); } } ``` ```tsx import {createCustomerClient__unstable} from '@shopify/hydrogen'; import * as remixBuild from '@remix-run/dev/server-build'; import { createRequestHandler, createCookieSessionStorage, type SessionStorage, type Session, } from '@shopify/remix-oxygen'; export default { async fetch( request: Request, env: Record<string, string>, executionContext: ExecutionContext, ) { const session = await HydrogenSession.init(request, [env.SESSION_SECRET]); /* Create a Customer API client with your credentials and options */ const customer = createCustomerClient__unstable({ /* 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, /* Public account URL for your store */ customerAccountUrl: env.PUBLIC_CUSTOMER_ACCOUNT_URL, request, session, }); const handleRequest = createRequestHandler({ build: remixBuild, mode: process.env.NODE_ENV, /* Inject the customer account client in the Remix context */ getLoadContext: () => ({customer}), }); return handleRequest(request); }, }; class HydrogenSession { 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.session.unset(key); } set(key: string, value: any) { this.session.set(key, value); } commit() { return this.sessionStorage.commitSession(this.session); } } ``` ## Props ### CreateCustomerClientGeneratedType #### Returns: CustomerClient #### Params: - input1: CustomerClientOptions export function createCustomerClient({ session, customerAccountId, customerAccountUrl, customerApiVersion = '2023-10', request, waitUntil, }: CustomerClientOptions): CustomerClient { if (!request?.url) { throw new Error( '[h2:error:createCustomerClient] The request object does not contain a URL.', ); } const url = new URL(request.url); const origin = url.protocol === 'http:' ? url.origin.replace('http', 'https') : url.origin; const locks: Locks = {}; const logSubRequestEvent = process.env.NODE_ENV === 'development' ? (query: string, startTime: number) => { (globalThis as any).__H2O_LOG_EVENT?.({ eventType: 'subrequest', url: `https://shopify.dev/?${hashKey([ `Customer Account `, /((query|mutation) [^\s\(]+)/g.exec(query)?.[0] || query.substring(0, 10), ])}`, startTime, waitUntil, ...getDebugHeaders(request), }); } : undefined; async function fetchCustomerAPI({ query, type, variables = {}, }: { query: string; type: 'query' | 'mutation'; variables?: Record; }) { const accessToken = session.get('customer_access_token'); const expiresAt = session.get('expires_at'); if (!accessToken || !expiresAt) throw new BadRequest( 'Unauthorized', 'Login before querying the Customer Account API.', ); await checkExpires({ locks, expiresAt, session, customerAccountId, customerAccountUrl, origin, }); const startTime = new Date().getTime(); const response = await fetch( `${customerAccountUrl}/account/customer/api/${customerApiVersion}/graphql`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': USER_AGENT, Origin: origin, Authorization: accessToken, }, body: JSON.stringify({ operationName: 'SomeQuery', query, variables, }), }, ); logSubRequestEvent?.(query, startTime); const body = await response.text(); const errorOptions: GraphQLErrorOptions = { response, type, query, queryVariables: variables, errors: undefined, client: 'customer', }; if (!response.ok) { /** * The Customer API might return a string error, or a JSON-formatted {error: string}. * We try both and conform them to a single {errors} format. */ let errors; try { errors = parseJSON(body); } catch (_e) { errors = [{message: body}]; } throwGraphQLError({...errorOptions, errors}); } try { return parseJSON(body).data; } catch (e) { throwGraphQLError({...errorOptions, errors: [{message: body}]}); } } return { login: async () => { const loginUrl = new URL(customerAccountUrl + '/auth/oauth/authorize'); const state = await generateState(); const nonce = await generateNonce(); loginUrl.searchParams.set('client_id', customerAccountId); loginUrl.searchParams.set('scope', 'openid email'); loginUrl.searchParams.append('response_type', 'code'); loginUrl.searchParams.append('redirect_uri', origin + '/authorize'); loginUrl.searchParams.set( 'scope', 'openid email https://api.customers.com/auth/customer.graphql', ); loginUrl.searchParams.append('state', state); loginUrl.searchParams.append('nonce', nonce); const verifier = await generateCodeVerifier(); const challenge = await generateCodeChallenge(verifier); session.set('code-verifier', verifier); session.set('state', state); session.set('nonce', nonce); loginUrl.searchParams.append('code_challenge', challenge); loginUrl.searchParams.append('code_challenge_method', 'S256'); return redirect(loginUrl.toString(), { headers: { 'Set-Cookie': await session.commit(), }, }); }, logout: async () => { const idToken = session.get('id_token'); clearSession(session); return redirect( `${customerAccountUrl}/auth/logout?id_token_hint=${idToken}`, { status: 302, headers: { 'Set-Cookie': await session.commit(), }, }, ); }, isLoggedIn: async () => { const expiresAt = session.get('expires_at'); if (!session.get('customer_access_token') || !expiresAt) return false; const startTime = new Date().getTime(); try { await checkExpires({ locks, expiresAt, session, customerAccountId, customerAccountUrl, origin, }); logSubRequestEvent?.(' check expires', startTime); } catch { return false; } return true; }, mutate(mutation, options) { mutation = minifyQuery(mutation); assertMutation(mutation, 'customer.mutate'); return fetchCustomerAPI({query: mutation, type: 'mutation', ...options}); }, query(query, options) { query = minifyQuery(query); assertQuery(query, 'customer.query'); return fetchCustomerAPI({query, type: 'query', ...options}); }, authorize: async (redirectPath = '/') => { const code = url.searchParams.get('code'); const state = url.searchParams.get('state'); if (!code || !state) { clearSession(session); throw new BadRequest( 'Unauthorized', 'No code or state parameter found in the redirect URL.', ); } if (session.get('state') !== state) { clearSession(session); throw new BadRequest( 'Unauthorized', 'The session state does not match the state parameter. Make sure that the session is configured correctly and passed to `createCustomerClient`.', ); } const clientId = customerAccountId; const body = new URLSearchParams(); body.append('grant_type', 'authorization_code'); body.append('client_id', clientId); body.append('redirect_uri', origin + '/authorize'); body.append('code', code); // Public Client const codeVerifier = session.get('code-verifier'); if (!codeVerifier) throw new BadRequest( 'Unauthorized', 'No code verifier found in the session. Make sure that the session is configured correctly and passed to `createCustomerClient`.', ); body.append('code_verifier', codeVerifier); const headers = { 'content-type': 'application/x-www-form-urlencoded', 'User-Agent': USER_AGENT, Origin: origin, }; const response = await fetch(`${customerAccountUrl}/auth/oauth/token`, { method: 'POST', headers, body, }); if (!response.ok) { throw new Response(await response.text(), { status: response.status, headers: { 'Content-Type': 'text/html; charset=utf-8', }, }); } const {access_token, expires_in, id_token, refresh_token} = await response.json(); const sessionNonce = session.get('nonce'); const responseNonce = await getNonce(id_token); if (sessionNonce !== responseNonce) { throw new BadRequest( 'Unauthorized', `Returned nonce does not match: ${sessionNonce} !== ${responseNonce}`, ); } session.set('customer_authorization_code_token', access_token); session.set( 'expires_at', new Date(new Date().getTime() + (expires_in! - 120) * 1000).getTime() + '', ); session.set('id_token', id_token); session.set('refresh_token', refresh_token); const customerAccessToken = await exchangeAccessToken( session, customerAccountId, customerAccountUrl, origin, ); session.set('customer_access_token', customerAccessToken); return redirect(redirectPath, { headers: { 'Set-Cookie': await session.commit(), }, }); }, }; } ### CustomerClientOptions ### session 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. ### customerAccountId Unique UUID prefixed with `shp_` associated with the application, this should be visible in the customer account api settings in the Hydrogen admin channel. ### customerAccountUrl The account URL associated with the application, this should be visible in the customer account api settings in the Hydrogen admin channel. ### customerApiVersion Override the version of the API ### request The object for the current Request. It should be provided by your platform. ### 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. ### HydrogenSession ### get ### set ### unset ### commit ### CrossRuntimeRequest ### url ### method ### headers ### CustomerClient ### login Start the OAuth login flow. This function should be called and returned from a Remix action. It redirects the user to a login domain. ### authorize On successful login, the user is redirect 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. ### isLoggedIn Returns if the user is logged in. It also checks if the access token is expired and refreshes it if needed. ### logout Logout the user by clearing the session and redirecting to the login domain. It should be called and returned from a Remix action. ### query Execute a GraphQL query against the Customer Account API. Usually you should first check if the user is logged in before querying the API. ### mutate Execute a GraphQL mutation against the Customer Account API. Usually you should first check if the user is logged in before querying the API. ## Related - [createStorefrontClient](/docs/api/hydrogen/2023-10/utilities/createstorefrontclient)