--- title: Authenticate customers with the Customer Account API description: >- Build an OAuth 2.0 authentication flow with PKCE to securely log customers into your app and query their account data through the Customer Account API. source_url: html: >- https://shopify.dev/docs/storefronts/headless/building-with-the-customer-account-api/authenticate-customers?extension=javascript md: >- https://shopify.dev/docs/storefronts/headless/building-with-the-customer-account-api/authenticate-customers.md?extension=javascript --- # Authenticate customers with the Customer Account API A customer authentication flow lets customers securely log in to your app and access their account data through the [Customer Account API](https://shopify.dev/docs/api/customer/latest). This tutorial shows you how to implement [OAuth 2.0 with PKCE (Proof Key for Code Exchange)](https://datatracker.ietf.org/doc/html/rfc7636)) to authenticate customers, store their access tokens, and query their order information. You'll build a complete authentication system that generates security parameters, exchanges authorization codes for access tokens, and makes authenticated GraphQL queries to retrieve customer data. ## What you'll learn In this tutorial, you'll learn how to: * Authenticate a customer with the Customer Account API. * Store authentication tokens securely in a database. * Query the [`Customer`](https://shopify.dev/docs/api/customer/latest/objects/customer) object from the Customer Account API. ## Requirements [Partner account](https://www.shopify.com/partners) [Development store](https://shopify.dev/docs/apps/build/dev-dashboard/development-stores) The development store should be pre-populated with [test data](https://shopify.dev/docs/api/development-stores/generated-test-data), including an order associated with the email address you'll use to log in to the customer account experience. [Shopify CLI](https://shopify.dev/docs/apps/build/cli-for-apps) You'll need to use the [latest version of Shopify CLI](https://shopify.dev/docs/api/shopify-cli#upgrade). [Scaffold an app](https://shopify.dev/docs/apps/build/scaffold-app) Scaffold a React Router app that uses Shopify CLI. [Request access to Protected Customer Data](https://shopify.dev/docs/apps/launch/protected-customer-data) The Customer Account API requires access to Level 2 Protected Customer Data. Specifically access to first name, last name and email. [Request access](https://shopify.dev/docs/apps/launch/protected-customer-data#request-access-to-protected-customer-data) for your app. ## Project JavaScript [View on GitHub](https://github.com/shopify/example-storefronts--customer-account-api-authentication--javascript) ## Create database tables for authorization credentials Set up your database to store PKCE values and customer access tokens—you'll need these tables to complete the OAuth flow and make authenticated API requests. ### Define Prisma schema for authorization credentials Create two models in `prisma/schema.prisma`. `CodeVerifier` stores temporary state and verifier values for PKCE flow, while `CustomerAccessToken` stores persistent access tokens for authenticating Customer Account API requests. ## /prisma/schema.prisma ```prisma // This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = "file:dev.sqlite" } model Session { id String @id shop String state String isOnline Boolean @default(false) scope String? expires DateTime? accessToken String userId BigInt? firstName String? lastName String? email String? accountOwner Boolean @default(false) locale String? collaborator Boolean? @default(false) emailVerified Boolean? @default(false) } model CodeVerifier { id String @id @default(cuid()) state String @unique verifier String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model CustomerAccessToken { id String @id @default(cuid()) shop String accessToken String expiresAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([shop]) } ``` ### Generate the database tables Run this migration command from your app's root directory to create the tables in your database: ## Terminal ```bash npx prisma migrate dev --name add_code_verifier_access_token ``` You should see a success message indicating your database is now in sync with your schema. ## Configure access scopes for your app Add the required access scopes to query customer data from the Customer Account API. In your app’s `shopify.app.toml` file, include the `customer_read_orders` and `customer_read_customers` access scopes to display a customer’s information using the Customer Account API. In a production app, you might need to add other access scopes, depending on your use case. Tip Request access to [protected customer data](https://shopify.dev/docs/apps/launch/protected-customer-data#request-access-to-protected-customer-data) and ensure you've requested the `Name` and `Email` fields in your app configuration. ## /shopify.app.toml ```toml # Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration client_id = "43edf630e3445d3371ff2f8da3256b9f" name = "example-customer-account-api-authentication" application_url = "https://" embedded = true [build] automatically_update_urls_on_dev = true include_config_on_deploy = true [webhooks] api_version = "2026-01" [[webhooks.subscriptions]] topics = [ "app/uninstalled" ] uri = "/webhooks/app/uninstalled" [[webhooks.subscriptions]] topics = [ "app/scopes_update" ] uri = "/webhooks/app/scopes_update" [access_scopes] # Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes scopes = "customer_read_orders,customer_read_customers" [auth] redirect_urls = [ "https://example.com/api/auth" ] [customer_authentication] redirect_uris = ["https:///customer-account-api/callback"] ``` ## Set up a tunnel URL and configure OAuth authentication Configure a public HTTPS domain for local development and set up Customer Account API authentication endpoints. The Customer Account API requires HTTPS and doesn't support localhost URLs. ### Create an ngrok tunnel with a static domain 1. Create an [ngrok account](https://ngrok.com/). 2. In your ngrok settings, add a [static domain](https://ngrok.com/docs/guides/how-to-set-up-a-custom-domain/). 3. Install the [ngrok CLI](https://ngrok.com/download). 4. In a terminal, start ngrok using the following command: ## Terminal ```bash ngrok http --domain= 3000 ``` ### Add Customer Account API authentication settings Add the `[customer_authentication]` module and `` to your `shopify.app.toml` file. This configures the OAuth 2.0 callback URL where customers return after authorization. Tip Your production app should discover authentication endpoints dynamically using [discovery endpoints](https://shopify.dev/docs/api/customer/latest#discovery-endpoints) instead of hardcoding them. ## /shopify.app.toml ```toml # Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration client_id = "43edf630e3445d3371ff2f8da3256b9f" name = "example-customer-account-api-authentication" application_url = "https://" embedded = true [build] automatically_update_urls_on_dev = true include_config_on_deploy = true [webhooks] api_version = "2026-01" [[webhooks.subscriptions]] topics = [ "app/uninstalled" ] uri = "/webhooks/app/uninstalled" [[webhooks.subscriptions]] topics = [ "app/scopes_update" ] uri = "/webhooks/app/scopes_update" [access_scopes] # Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes scopes = "customer_read_orders,customer_read_customers" [auth] redirect_urls = [ "https://example.com/api/auth" ] [customer_authentication] redirect_uris = ["https:///customer-account-api/callback"] ``` ### Generate the environment file 1. Run the following command to generate an `.env` file containing your app's environment variables: ## Terminal ```bash shopify app env pull ``` 1. Add your development storefront domain to this file and save: ## Terminal ```bash SHOP_STOREFRONT_DOMAIN=your-dev-store.myshopify.com ``` Note Don't include `https://` in the domain value. ## Initiate the OAuth authorization flow Set up the authorization route to generate PKCE security parameters and redirect customers to Shopify's login page. This starts the secure OAuth flow for obtaining customer access tokens. ### Create the authorization route file In your app's `app/routes` folder create a new file: `customer-account-api.auth.jsx`. This route handles the initial OAuth authorization request and generates the required security parameters. ### Fetch Open​ID configuration dynamically Retrieve the shop's OpenID configuration to get the `authorization_endpoint` URL. This endpoint points to Shopify's login page where customers authenticate. ## /app/routes/customer-account-api.auth.jsx ```jsx import { redirect, useLoaderData } from "react-router"; import crypto from "crypto"; import prisma from "../db.server"; function generateCodeVerifier() { return crypto.randomBytes(32).toString("base64url"); } function generateCodeChallenge(verifier) { return crypto.createHash("sha256").update(verifier).digest("base64url"); } function generateState() { return crypto.randomBytes(16).toString("base64url"); } export const loader = async ({ request }) => { try { // Fetch OpenID configuration const openidConfigUrl = `https://${process.env.SHOP_STOREFRONT_DOMAIN}/.well-known/openid-configuration`; const openidResponse = await fetch(openidConfigUrl); if (!openidResponse.ok) { throw new Error(`Failed to fetch OpenID configuration: ${openidResponse.statusText}`); } const openidConfig = await openidResponse.json(); const authorizationEndpoint = openidConfig.authorization_endpoint; // Generate PKCE parameters const codeVerifier = generateCodeVerifier(); const codeChallenge = generateCodeChallenge(codeVerifier); const state = generateState(); // Store code verifier in database await prisma.codeVerifier.create({ data: { state, verifier: codeVerifier, }, }); // Get the callback URL from the request const url = new URL(request.url); const callbackUrl = `https://${url.host}/customer-account-api/callback`; // Get client_id from environment or config const clientId = process.env.SHOPIFY_API_KEY; // Build authorization URL const authUrl = new URL(authorizationEndpoint); authUrl.searchParams.set("client_id", clientId); authUrl.searchParams.set("response_type", "code"); authUrl.searchParams.set("redirect_uri", callbackUrl); authUrl.searchParams.set("scope", "openid email customer-account-api:full"); authUrl.searchParams.set("state", state); authUrl.searchParams.set("code_challenge", codeChallenge); authUrl.searchParams.set("code_challenge_method", "S256"); // Redirect directly to the authorization URL return redirect(authUrl.toString()); } catch (error) { console.error("Error generating auth URL:", error); // If there's an error, return error data to display return { error: error.message, }; } }; export default function CustomerAccountApiAuth() { const data = useLoaderData(); // Only renders if there's an error (otherwise loader redirects) return (

Customer Account API - Authentication Error

Error

{data?.error || "An unexpected error occurred"}

Try again
); } ``` ### Define PKCE helper functions Define helper functions to generate OAuth security parameters. These functions create a random `code_verifier`, compute its SHA256 `code_challenge`, and generate a `state` token for CSRF protection. ## /app/routes/customer-account-api.auth.jsx ```jsx import { redirect, useLoaderData } from "react-router"; import crypto from "crypto"; import prisma from "../db.server"; function generateCodeVerifier() { return crypto.randomBytes(32).toString("base64url"); } function generateCodeChallenge(verifier) { return crypto.createHash("sha256").update(verifier).digest("base64url"); } function generateState() { return crypto.randomBytes(16).toString("base64url"); } export const loader = async ({ request }) => { try { // Fetch OpenID configuration const openidConfigUrl = `https://${process.env.SHOP_STOREFRONT_DOMAIN}/.well-known/openid-configuration`; const openidResponse = await fetch(openidConfigUrl); if (!openidResponse.ok) { throw new Error(`Failed to fetch OpenID configuration: ${openidResponse.statusText}`); } const openidConfig = await openidResponse.json(); const authorizationEndpoint = openidConfig.authorization_endpoint; // Generate PKCE parameters const codeVerifier = generateCodeVerifier(); const codeChallenge = generateCodeChallenge(codeVerifier); const state = generateState(); // Store code verifier in database await prisma.codeVerifier.create({ data: { state, verifier: codeVerifier, }, }); // Get the callback URL from the request const url = new URL(request.url); const callbackUrl = `https://${url.host}/customer-account-api/callback`; // Get client_id from environment or config const clientId = process.env.SHOPIFY_API_KEY; // Build authorization URL const authUrl = new URL(authorizationEndpoint); authUrl.searchParams.set("client_id", clientId); authUrl.searchParams.set("response_type", "code"); authUrl.searchParams.set("redirect_uri", callbackUrl); authUrl.searchParams.set("scope", "openid email customer-account-api:full"); authUrl.searchParams.set("state", state); authUrl.searchParams.set("code_challenge", codeChallenge); authUrl.searchParams.set("code_challenge_method", "S256"); // Redirect directly to the authorization URL return redirect(authUrl.toString()); } catch (error) { console.error("Error generating auth URL:", error); // If there's an error, return error data to display return { error: error.message, }; } }; export default function CustomerAccountApiAuth() { const data = useLoaderData(); // Only renders if there's an error (otherwise loader redirects) return (

Customer Account API - Authentication Error

Error

{data?.error || "An unexpected error occurred"}

Try again
); } ``` ### Generate security parameters and store code verifier Call the helper functions to generate the PKCE values, then save the `code_verifier` to your database using `state` as the lookup key. You'll retrieve this verifier in the callback handler to securely complete the token exchange. ## /app/routes/customer-account-api.auth.jsx ```jsx import { redirect, useLoaderData } from "react-router"; import crypto from "crypto"; import prisma from "../db.server"; function generateCodeVerifier() { return crypto.randomBytes(32).toString("base64url"); } function generateCodeChallenge(verifier) { return crypto.createHash("sha256").update(verifier).digest("base64url"); } function generateState() { return crypto.randomBytes(16).toString("base64url"); } export const loader = async ({ request }) => { try { // Fetch OpenID configuration const openidConfigUrl = `https://${process.env.SHOP_STOREFRONT_DOMAIN}/.well-known/openid-configuration`; const openidResponse = await fetch(openidConfigUrl); if (!openidResponse.ok) { throw new Error(`Failed to fetch OpenID configuration: ${openidResponse.statusText}`); } const openidConfig = await openidResponse.json(); const authorizationEndpoint = openidConfig.authorization_endpoint; // Generate PKCE parameters const codeVerifier = generateCodeVerifier(); const codeChallenge = generateCodeChallenge(codeVerifier); const state = generateState(); // Store code verifier in database await prisma.codeVerifier.create({ data: { state, verifier: codeVerifier, }, }); // Get the callback URL from the request const url = new URL(request.url); const callbackUrl = `https://${url.host}/customer-account-api/callback`; // Get client_id from environment or config const clientId = process.env.SHOPIFY_API_KEY; // Build authorization URL const authUrl = new URL(authorizationEndpoint); authUrl.searchParams.set("client_id", clientId); authUrl.searchParams.set("response_type", "code"); authUrl.searchParams.set("redirect_uri", callbackUrl); authUrl.searchParams.set("scope", "openid email customer-account-api:full"); authUrl.searchParams.set("state", state); authUrl.searchParams.set("code_challenge", codeChallenge); authUrl.searchParams.set("code_challenge_method", "S256"); // Redirect directly to the authorization URL return redirect(authUrl.toString()); } catch (error) { console.error("Error generating auth URL:", error); // If there's an error, return error data to display return { error: error.message, }; } }; export default function CustomerAccountApiAuth() { const data = useLoaderData(); // Only renders if there's an error (otherwise loader redirects) return (

Customer Account API - Authentication Error

Error

{data?.error || "An unexpected error occurred"}

Try again
); } ``` ### Redirect to Shopify's authorization endpoint Construct the authorization URL with required OAuth parameters and redirect the customer to Shopify's login page. After authentication, Shopify redirects back to your callback URL with an authorization code. ## /app/routes/customer-account-api.auth.jsx ```jsx import { redirect, useLoaderData } from "react-router"; import crypto from "crypto"; import prisma from "../db.server"; function generateCodeVerifier() { return crypto.randomBytes(32).toString("base64url"); } function generateCodeChallenge(verifier) { return crypto.createHash("sha256").update(verifier).digest("base64url"); } function generateState() { return crypto.randomBytes(16).toString("base64url"); } export const loader = async ({ request }) => { try { // Fetch OpenID configuration const openidConfigUrl = `https://${process.env.SHOP_STOREFRONT_DOMAIN}/.well-known/openid-configuration`; const openidResponse = await fetch(openidConfigUrl); if (!openidResponse.ok) { throw new Error(`Failed to fetch OpenID configuration: ${openidResponse.statusText}`); } const openidConfig = await openidResponse.json(); const authorizationEndpoint = openidConfig.authorization_endpoint; // Generate PKCE parameters const codeVerifier = generateCodeVerifier(); const codeChallenge = generateCodeChallenge(codeVerifier); const state = generateState(); // Store code verifier in database await prisma.codeVerifier.create({ data: { state, verifier: codeVerifier, }, }); // Get the callback URL from the request const url = new URL(request.url); const callbackUrl = `https://${url.host}/customer-account-api/callback`; // Get client_id from environment or config const clientId = process.env.SHOPIFY_API_KEY; // Build authorization URL const authUrl = new URL(authorizationEndpoint); authUrl.searchParams.set("client_id", clientId); authUrl.searchParams.set("response_type", "code"); authUrl.searchParams.set("redirect_uri", callbackUrl); authUrl.searchParams.set("scope", "openid email customer-account-api:full"); authUrl.searchParams.set("state", state); authUrl.searchParams.set("code_challenge", codeChallenge); authUrl.searchParams.set("code_challenge_method", "S256"); // Redirect directly to the authorization URL return redirect(authUrl.toString()); } catch (error) { console.error("Error generating auth URL:", error); // If there's an error, return error data to display return { error: error.message, }; } }; export default function CustomerAccountApiAuth() { const data = useLoaderData(); // Only renders if there's an error (otherwise loader redirects) return (

Customer Account API - Authentication Error

Error

{data?.error || "An unexpected error occurred"}

Try again
); } ``` ## Configure session management for customer authentication Set up cookie-based session storage to persist customer login state across pages. In your app's `app` folder, create a new file: `sessions.server.jsx`. This file provides helper functions that store and retrieve the `customerTokenId` in an encrypted, HTTP-only cookie. The cookie persists the customer's login state across pages in your app. ## /app/sessions.server.js ```javascript import { createCookieSessionStorage } from "react-router"; // Customer session storage for Customer Account API authentication // Stores customer access token ID in an encrypted, HTTP-only cookie export const customerSessionStorage = createCookieSessionStorage({ cookie: { name: "__customer_session", httpOnly: true, path: "/", sameSite: "lax", secrets: [process.env.SESSION_SECRET || "default-secret-change-in-production"], secure: process.env.NODE_ENV === "production", maxAge: 60 * 60, // 1 hour (3600 seconds) }, }); // Get customer token ID from session export async function getCustomerTokenId(request) { const session = await customerSessionStorage.getSession( request.headers.get("Cookie") ); return session.get("customerTokenId"); } // Set customer token ID in session export async function setCustomerTokenId(request, tokenId) { const session = await customerSessionStorage.getSession( request.headers.get("Cookie") ); session.set("customerTokenId", tokenId); return customerSessionStorage.commitSession(session); } // Destroy customer session export async function destroyCustomerSession(request) { const session = await customerSessionStorage.getSession( request.headers.get("Cookie") ); return customerSessionStorage.destroySession(session); } ``` ## Generate and store the customer access token Set up the OAuth callback handler to exchange the authorization code for a customer access token. This completes the PKCE flow and establishes an authenticated session for Customer Account API requests. ### Create the callback route file In your app's `app/routes` folder create a new file: `customer-account-api.callback.jsx`. This route handles Shopify's OAuth redirect after customer authentication. ### Extract authorization parameters from the callback URL Extract the `code` and `state` parameters from the callback URL. The `code` will be exchanged with your stored `code_verifier` for an access token, while `state` must match your original value to prevent CSRF attacks. ## /app/routes/customer-account-api.callback.jsx ```jsx import { redirect } from "react-router"; import { useLoaderData } from "react-router"; import prisma from "../db.server"; import { setCustomerTokenId } from "../sessions.server"; export const loader = async ({ request }) => { try { const url = new URL(request.url); const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); if (!code || !state) { throw new Error("Missing code or state parameter"); } // Retrieve the code verifier from the database const codeVerifierRecord = await prisma.codeVerifier.findUnique({ where: { state }, }); if (!codeVerifierRecord) { throw new Error("Invalid state parameter or code verifier not found"); } // Fetch OpenID configuration to get token endpoint const openidConfigUrl = `https://${process.env.SHOP_STOREFRONT_DOMAIN}/.well-known/openid-configuration`; const openidResponse = await fetch(openidConfigUrl); if (!openidResponse.ok) { throw new Error(`Failed to fetch OpenID configuration: ${openidResponse.statusText}`); } const openidConfig = await openidResponse.json(); const tokenEndpoint = openidConfig.token_endpoint; // Get callback URL const callbackUrl = `https://${url.host}/customer-account-api/callback`; // Get client_id from environment or config const clientId = process.env.SHOPIFY_API_KEY; // Exchange authorization code for access token const tokenResponse = await fetch(tokenEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ grant_type: "authorization_code", client_id: clientId, redirect_uri: callbackUrl, code: code, code_verifier: codeVerifierRecord.verifier, }), }); if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); throw new Error(`Token exchange failed: ${tokenResponse.statusText} - ${errorText}`); } const tokenData = await tokenResponse.json(); // Calculate token expiration const expiresAt = tokenData.expires_in ? new Date(Date.now() + tokenData.expires_in * 1000) : null; // Store the access token in the database const customerAccessToken = await prisma.customerAccessToken.create({ data: { shop: process.env.SHOP_STOREFRONT_DOMAIN, accessToken: tokenData.access_token, expiresAt, }, }); // Clean up the used code verifier await prisma.codeVerifier.delete({ where: { state }, }); // Store tokenId in session cookie and redirect to order list const setCookieHeader = await setCustomerTokenId(request, customerAccessToken.id); return redirect("/customer-account-api/order-list", { headers: { "Set-Cookie": setCookieHeader, }, }); } catch (error) { console.error("Error in callback:", error); return { success: false, error: error.message, }; } }; export default function CustomerAccountApiCallback() { const data = useLoaderData(); // Only renders if there's an error (otherwise loader redirects) return (

Customer Account API - Callback Error

Authentication Error

{data?.error || "An error occurred during authentication"}

Try again
); } ``` ### Retrieve the stored code verifier Query your database for the `code_verifier` (stored during authorization) using `state` as the lookup key. You'll need this verifier to securely exchange the authorization code for an access token in the next step. ## /app/routes/customer-account-api.callback.jsx ```jsx import { redirect } from "react-router"; import { useLoaderData } from "react-router"; import prisma from "../db.server"; import { setCustomerTokenId } from "../sessions.server"; export const loader = async ({ request }) => { try { const url = new URL(request.url); const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); if (!code || !state) { throw new Error("Missing code or state parameter"); } // Retrieve the code verifier from the database const codeVerifierRecord = await prisma.codeVerifier.findUnique({ where: { state }, }); if (!codeVerifierRecord) { throw new Error("Invalid state parameter or code verifier not found"); } // Fetch OpenID configuration to get token endpoint const openidConfigUrl = `https://${process.env.SHOP_STOREFRONT_DOMAIN}/.well-known/openid-configuration`; const openidResponse = await fetch(openidConfigUrl); if (!openidResponse.ok) { throw new Error(`Failed to fetch OpenID configuration: ${openidResponse.statusText}`); } const openidConfig = await openidResponse.json(); const tokenEndpoint = openidConfig.token_endpoint; // Get callback URL const callbackUrl = `https://${url.host}/customer-account-api/callback`; // Get client_id from environment or config const clientId = process.env.SHOPIFY_API_KEY; // Exchange authorization code for access token const tokenResponse = await fetch(tokenEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ grant_type: "authorization_code", client_id: clientId, redirect_uri: callbackUrl, code: code, code_verifier: codeVerifierRecord.verifier, }), }); if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); throw new Error(`Token exchange failed: ${tokenResponse.statusText} - ${errorText}`); } const tokenData = await tokenResponse.json(); // Calculate token expiration const expiresAt = tokenData.expires_in ? new Date(Date.now() + tokenData.expires_in * 1000) : null; // Store the access token in the database const customerAccessToken = await prisma.customerAccessToken.create({ data: { shop: process.env.SHOP_STOREFRONT_DOMAIN, accessToken: tokenData.access_token, expiresAt, }, }); // Clean up the used code verifier await prisma.codeVerifier.delete({ where: { state }, }); // Store tokenId in session cookie and redirect to order list const setCookieHeader = await setCustomerTokenId(request, customerAccessToken.id); return redirect("/customer-account-api/order-list", { headers: { "Set-Cookie": setCookieHeader, }, }); } catch (error) { console.error("Error in callback:", error); return { success: false, error: error.message, }; } }; export default function CustomerAccountApiCallback() { const data = useLoaderData(); // Only renders if there's an error (otherwise loader redirects) return (

Customer Account API - Callback Error

Authentication Error

{data?.error || "An error occurred during authentication"}

Try again
); } ``` ### Fetch the token endpoint dynamically Discover the token endpoint by fetching the shop's OpenID configuration to retrieve the `token_endpoint` URL. This endpoint will be used to exchange your authorization code for an access token. ## /app/routes/customer-account-api.callback.jsx ```jsx import { redirect } from "react-router"; import { useLoaderData } from "react-router"; import prisma from "../db.server"; import { setCustomerTokenId } from "../sessions.server"; export const loader = async ({ request }) => { try { const url = new URL(request.url); const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); if (!code || !state) { throw new Error("Missing code or state parameter"); } // Retrieve the code verifier from the database const codeVerifierRecord = await prisma.codeVerifier.findUnique({ where: { state }, }); if (!codeVerifierRecord) { throw new Error("Invalid state parameter or code verifier not found"); } // Fetch OpenID configuration to get token endpoint const openidConfigUrl = `https://${process.env.SHOP_STOREFRONT_DOMAIN}/.well-known/openid-configuration`; const openidResponse = await fetch(openidConfigUrl); if (!openidResponse.ok) { throw new Error(`Failed to fetch OpenID configuration: ${openidResponse.statusText}`); } const openidConfig = await openidResponse.json(); const tokenEndpoint = openidConfig.token_endpoint; // Get callback URL const callbackUrl = `https://${url.host}/customer-account-api/callback`; // Get client_id from environment or config const clientId = process.env.SHOPIFY_API_KEY; // Exchange authorization code for access token const tokenResponse = await fetch(tokenEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ grant_type: "authorization_code", client_id: clientId, redirect_uri: callbackUrl, code: code, code_verifier: codeVerifierRecord.verifier, }), }); if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); throw new Error(`Token exchange failed: ${tokenResponse.statusText} - ${errorText}`); } const tokenData = await tokenResponse.json(); // Calculate token expiration const expiresAt = tokenData.expires_in ? new Date(Date.now() + tokenData.expires_in * 1000) : null; // Store the access token in the database const customerAccessToken = await prisma.customerAccessToken.create({ data: { shop: process.env.SHOP_STOREFRONT_DOMAIN, accessToken: tokenData.access_token, expiresAt, }, }); // Clean up the used code verifier await prisma.codeVerifier.delete({ where: { state }, }); // Store tokenId in session cookie and redirect to order list const setCookieHeader = await setCustomerTokenId(request, customerAccessToken.id); return redirect("/customer-account-api/order-list", { headers: { "Set-Cookie": setCookieHeader, }, }); } catch (error) { console.error("Error in callback:", error); return { success: false, error: error.message, }; } }; export default function CustomerAccountApiCallback() { const data = useLoaderData(); // Only renders if there's an error (otherwise loader redirects) return (

Customer Account API - Callback Error

Authentication Error

{data?.error || "An error occurred during authentication"}

Try again
); } ``` ### Exchange code for a customer access token Make a POST request to the token endpoint with your authorization code and `code_verifier`. This returns a `customerAccessToken` that authenticates all subsequent Customer Account API requests. ## /app/routes/customer-account-api.callback.jsx ```jsx import { redirect } from "react-router"; import { useLoaderData } from "react-router"; import prisma from "../db.server"; import { setCustomerTokenId } from "../sessions.server"; export const loader = async ({ request }) => { try { const url = new URL(request.url); const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); if (!code || !state) { throw new Error("Missing code or state parameter"); } // Retrieve the code verifier from the database const codeVerifierRecord = await prisma.codeVerifier.findUnique({ where: { state }, }); if (!codeVerifierRecord) { throw new Error("Invalid state parameter or code verifier not found"); } // Fetch OpenID configuration to get token endpoint const openidConfigUrl = `https://${process.env.SHOP_STOREFRONT_DOMAIN}/.well-known/openid-configuration`; const openidResponse = await fetch(openidConfigUrl); if (!openidResponse.ok) { throw new Error(`Failed to fetch OpenID configuration: ${openidResponse.statusText}`); } const openidConfig = await openidResponse.json(); const tokenEndpoint = openidConfig.token_endpoint; // Get callback URL const callbackUrl = `https://${url.host}/customer-account-api/callback`; // Get client_id from environment or config const clientId = process.env.SHOPIFY_API_KEY; // Exchange authorization code for access token const tokenResponse = await fetch(tokenEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ grant_type: "authorization_code", client_id: clientId, redirect_uri: callbackUrl, code: code, code_verifier: codeVerifierRecord.verifier, }), }); if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); throw new Error(`Token exchange failed: ${tokenResponse.statusText} - ${errorText}`); } const tokenData = await tokenResponse.json(); // Calculate token expiration const expiresAt = tokenData.expires_in ? new Date(Date.now() + tokenData.expires_in * 1000) : null; // Store the access token in the database const customerAccessToken = await prisma.customerAccessToken.create({ data: { shop: process.env.SHOP_STOREFRONT_DOMAIN, accessToken: tokenData.access_token, expiresAt, }, }); // Clean up the used code verifier await prisma.codeVerifier.delete({ where: { state }, }); // Store tokenId in session cookie and redirect to order list const setCookieHeader = await setCustomerTokenId(request, customerAccessToken.id); return redirect("/customer-account-api/order-list", { headers: { "Set-Cookie": setCookieHeader, }, }); } catch (error) { console.error("Error in callback:", error); return { success: false, error: error.message, }; } }; export default function CustomerAccountApiCallback() { const data = useLoaderData(); // Only renders if there's an error (otherwise loader redirects) return (

Customer Account API - Callback Error

Authentication Error

{data?.error || "An error occurred during authentication"}

Try again
); } ``` ### Store access token and delete used code verifier Store the `customerAccessToken` in your database along with its expiration time. After storing, delete the used `code_verifier` from the database to prevent replay attacks. ## /app/routes/customer-account-api.callback.jsx ```jsx import { redirect } from "react-router"; import { useLoaderData } from "react-router"; import prisma from "../db.server"; import { setCustomerTokenId } from "../sessions.server"; export const loader = async ({ request }) => { try { const url = new URL(request.url); const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); if (!code || !state) { throw new Error("Missing code or state parameter"); } // Retrieve the code verifier from the database const codeVerifierRecord = await prisma.codeVerifier.findUnique({ where: { state }, }); if (!codeVerifierRecord) { throw new Error("Invalid state parameter or code verifier not found"); } // Fetch OpenID configuration to get token endpoint const openidConfigUrl = `https://${process.env.SHOP_STOREFRONT_DOMAIN}/.well-known/openid-configuration`; const openidResponse = await fetch(openidConfigUrl); if (!openidResponse.ok) { throw new Error(`Failed to fetch OpenID configuration: ${openidResponse.statusText}`); } const openidConfig = await openidResponse.json(); const tokenEndpoint = openidConfig.token_endpoint; // Get callback URL const callbackUrl = `https://${url.host}/customer-account-api/callback`; // Get client_id from environment or config const clientId = process.env.SHOPIFY_API_KEY; // Exchange authorization code for access token const tokenResponse = await fetch(tokenEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ grant_type: "authorization_code", client_id: clientId, redirect_uri: callbackUrl, code: code, code_verifier: codeVerifierRecord.verifier, }), }); if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); throw new Error(`Token exchange failed: ${tokenResponse.statusText} - ${errorText}`); } const tokenData = await tokenResponse.json(); // Calculate token expiration const expiresAt = tokenData.expires_in ? new Date(Date.now() + tokenData.expires_in * 1000) : null; // Store the access token in the database const customerAccessToken = await prisma.customerAccessToken.create({ data: { shop: process.env.SHOP_STOREFRONT_DOMAIN, accessToken: tokenData.access_token, expiresAt, }, }); // Clean up the used code verifier await prisma.codeVerifier.delete({ where: { state }, }); // Store tokenId in session cookie and redirect to order list const setCookieHeader = await setCustomerTokenId(request, customerAccessToken.id); return redirect("/customer-account-api/order-list", { headers: { "Set-Cookie": setCookieHeader, }, }); } catch (error) { console.error("Error in callback:", error); return { success: false, error: error.message, }; } }; export default function CustomerAccountApiCallback() { const data = useLoaderData(); // Only renders if there's an error (otherwise loader redirects) return (

Customer Account API - Callback Error

Authentication Error

{data?.error || "An error occurred during authentication"}

Try again
); } ``` ### Set session cookie and redirect to authenticated page Store the `tokenId` in a session cookie and redirect the customer to your app's authenticated page. Tip For this tutorial, you'll redirect to an order list page, but in production you can choose any destination appropriate for your app. ## /app/routes/customer-account-api.callback.jsx ```jsx import { redirect } from "react-router"; import { useLoaderData } from "react-router"; import prisma from "../db.server"; import { setCustomerTokenId } from "../sessions.server"; export const loader = async ({ request }) => { try { const url = new URL(request.url); const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); if (!code || !state) { throw new Error("Missing code or state parameter"); } // Retrieve the code verifier from the database const codeVerifierRecord = await prisma.codeVerifier.findUnique({ where: { state }, }); if (!codeVerifierRecord) { throw new Error("Invalid state parameter or code verifier not found"); } // Fetch OpenID configuration to get token endpoint const openidConfigUrl = `https://${process.env.SHOP_STOREFRONT_DOMAIN}/.well-known/openid-configuration`; const openidResponse = await fetch(openidConfigUrl); if (!openidResponse.ok) { throw new Error(`Failed to fetch OpenID configuration: ${openidResponse.statusText}`); } const openidConfig = await openidResponse.json(); const tokenEndpoint = openidConfig.token_endpoint; // Get callback URL const callbackUrl = `https://${url.host}/customer-account-api/callback`; // Get client_id from environment or config const clientId = process.env.SHOPIFY_API_KEY; // Exchange authorization code for access token const tokenResponse = await fetch(tokenEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ grant_type: "authorization_code", client_id: clientId, redirect_uri: callbackUrl, code: code, code_verifier: codeVerifierRecord.verifier, }), }); if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); throw new Error(`Token exchange failed: ${tokenResponse.statusText} - ${errorText}`); } const tokenData = await tokenResponse.json(); // Calculate token expiration const expiresAt = tokenData.expires_in ? new Date(Date.now() + tokenData.expires_in * 1000) : null; // Store the access token in the database const customerAccessToken = await prisma.customerAccessToken.create({ data: { shop: process.env.SHOP_STOREFRONT_DOMAIN, accessToken: tokenData.access_token, expiresAt, }, }); // Clean up the used code verifier await prisma.codeVerifier.delete({ where: { state }, }); // Store tokenId in session cookie and redirect to order list const setCookieHeader = await setCustomerTokenId(request, customerAccessToken.id); return redirect("/customer-account-api/order-list", { headers: { "Set-Cookie": setCookieHeader, }, }); } catch (error) { console.error("Error in callback:", error); return { success: false, error: error.message, }; } }; export default function CustomerAccountApiCallback() { const data = useLoaderData(); // Only renders if there's an error (otherwise loader redirects) return (

Customer Account API - Callback Error

Authentication Error

{data?.error || "An error occurred during authentication"}

Try again
); } ``` ## Query the Customer Account API with the customer access token Set up an authenticated route that retrieves customer data from the Customer Account API. This demonstrates how to use the stored access token to make GraphQL queries on behalf of the authenticated customer. ### Create the order list route file In your app's `app/routes` folder, create a new file: `customer-account-api.order-list.jsx`. This route displays customer order data by querying the Customer Account API. ### Retrieve the customer token from session Get the `customerTokenId` from the session cookie. If no token is found, redirect the customer to authenticate. ## /app/routes/customer-account-api.order-list.jsx ```jsx import { useLoaderData } from "react-router"; import { useState, useEffect } from "react"; import prisma from "../db.server"; import { getCustomerTokenId } from "../sessions.server"; export const loader = async ({ request }) => { try { // Get tokenId from session cookie const tokenId = await getCustomerTokenId(request); if (!tokenId) { throw new Error("No customer authentication found. Please authenticate first."); } // Fetch the access token from the database const customerAccessToken = await prisma.customerAccessToken.findUnique({ where: { id: tokenId }, }); if (!customerAccessToken) { throw new Error("Access token not found"); } // Check if token is expired if (customerAccessToken.expiresAt && new Date() > customerAccessToken.expiresAt) { throw new Error("Access token has expired"); } // Get shop domain for client-side API calls const shopDomain = process.env.SHOP_STOREFRONT_DOMAIN; // Fetch Customer Account API configuration const wellKnownUrl = `https://${shopDomain}/.well-known/customer-account-api`; const wellKnownResponse = await fetch(wellKnownUrl); if (!wellKnownResponse.ok) { ``` ### Fetch the access token from your database Fetch the `customerAccessToken` from your database using the `tokenId` as the lookup key. Verify the token exists and hasn't expired before proceeding with API requests. ## /app/routes/customer-account-api.order-list.jsx ```jsx import { useLoaderData } from "react-router"; import { useState, useEffect } from "react"; import prisma from "../db.server"; import { getCustomerTokenId } from "../sessions.server"; export const loader = async ({ request }) => { try { // Get tokenId from session cookie const tokenId = await getCustomerTokenId(request); if (!tokenId) { throw new Error("No customer authentication found. Please authenticate first."); } // Fetch the access token from the database const customerAccessToken = await prisma.customerAccessToken.findUnique({ where: { id: tokenId }, }); if (!customerAccessToken) { throw new Error("Access token not found"); } // Check if token is expired if (customerAccessToken.expiresAt && new Date() > customerAccessToken.expiresAt) { throw new Error("Access token has expired"); } // Get shop domain for client-side API calls const shopDomain = process.env.SHOP_STOREFRONT_DOMAIN; // Fetch Customer Account API configuration const wellKnownUrl = `https://${shopDomain}/.well-known/customer-account-api`; const wellKnownResponse = await fetch(wellKnownUrl); if (!wellKnownResponse.ok) { ``` ### Discover the Graph​QL API endpoint Fetch the Customer Account API configuration to dynamically retrieve the `graphql_api` endpoint URL. This endpoint handles all Customer Account API GraphQL queries. ## /app/routes/customer-account-api.order-list.jsx ```jsx import { useLoaderData } from "react-router"; import { useState, useEffect } from "react"; import prisma from "../db.server"; import { getCustomerTokenId } from "../sessions.server"; export const loader = async ({ request }) => { try { // Get tokenId from session cookie const tokenId = await getCustomerTokenId(request); if (!tokenId) { throw new Error("No customer authentication found. Please authenticate first."); } // Fetch the access token from the database const customerAccessToken = await prisma.customerAccessToken.findUnique({ where: { id: tokenId }, }); if (!customerAccessToken) { throw new Error("Access token not found"); } // Check if token is expired if (customerAccessToken.expiresAt && new Date() > customerAccessToken.expiresAt) { throw new Error("Access token has expired"); } // Get shop domain for client-side API calls const shopDomain = process.env.SHOP_STOREFRONT_DOMAIN; // Fetch Customer Account API configuration const wellKnownUrl = `https://${shopDomain}/.well-known/customer-account-api`; const wellKnownResponse = await fetch(wellKnownUrl); if (!wellKnownResponse.ok) { ``` ### Query the Customer Account API Send a `POST` request to the GraphQL endpoint with the `customerAccessToken` in the Authorization header. The API returns customer order data that's displayed on the order list page. ## /app/routes/customer-account-api.order-list.jsx ```jsx import { useLoaderData } from "react-router"; import { useState, useEffect } from "react"; import prisma from "../db.server"; import { getCustomerTokenId } from "../sessions.server"; export const loader = async ({ request }) => { try { // Get tokenId from session cookie const tokenId = await getCustomerTokenId(request); if (!tokenId) { throw new Error("No customer authentication found. Please authenticate first."); } // Fetch the access token from the database const customerAccessToken = await prisma.customerAccessToken.findUnique({ where: { id: tokenId }, }); if (!customerAccessToken) { throw new Error("Access token not found"); } // Check if token is expired if (customerAccessToken.expiresAt && new Date() > customerAccessToken.expiresAt) { throw new Error("Access token has expired"); } // Get shop domain for client-side API calls const shopDomain = process.env.SHOP_STOREFRONT_DOMAIN; // Fetch Customer Account API configuration const wellKnownUrl = `https://${shopDomain}/.well-known/customer-account-api`; const wellKnownResponse = await fetch(wellKnownUrl); if (!wellKnownResponse.ok) { ``` ## Preview the app Use the Shopify CLI to preview your app to make sure that it works as expected. 1. In a terminal, navigate to your app directory. 2. Start `app dev` with your tunnel URL to build and preview your app. ```bash shopify app dev --tunnel-url=https://:3000 ``` 1. Select the dev store you declared as your `SHOP_STOREFRONT_DOMAIN` variable from the [Generate the environment file](#generate-the-environment-file) step. 2. Visit `https:///customer-account-api/auth`. You'll be redirected to the login page. After entering your email and one-time-passcode, you'll be redirected to a basic portal showing your customer name, email and orders. Troubleshooting ##### Invalid client\_​id Ensure the `client_id` matches the `client_id` in your `shopify.app.toml` file. ##### Invalid redirect\_​uri Ensure the `[customer_authentication]` module is included in your `shopify.app.toml` file and the `redirect_uri` matches the tunnel url. Do not use a local host URL or http. ##### Invalid client credentials Ensure the test shop you've installed the app on matches the `SHOP_STOREFRONT_DOMAIN` in your `.env` file. ##### Customer name is not displayed Ensure you've requested access to Protected Customer Data and the name and email fields in your [Dev Dashboard](https://shopify.dev/docs/apps/build/dev-dashboard). Check your `shopify.app.toml` file for the required scopes. ## /prisma/schema.prisma ```prisma // This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = "file:dev.sqlite" } model Session { id String @id shop String state String isOnline Boolean @default(false) scope String? expires DateTime? accessToken String userId BigInt? firstName String? lastName String? email String? accountOwner Boolean @default(false) locale String? collaborator Boolean? @default(false) emailVerified Boolean? @default(false) } model CodeVerifier { id String @id @default(cuid()) state String @unique verifier String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model CustomerAccessToken { id String @id @default(cuid()) shop String accessToken String expiresAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([shop]) } ``` ## /shopify.app.toml ```toml # Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration client_id = "43edf630e3445d3371ff2f8da3256b9f" name = "example-customer-account-api-authentication" application_url = "https://" embedded = true [build] automatically_update_urls_on_dev = true include_config_on_deploy = true [webhooks] api_version = "2026-01" [[webhooks.subscriptions]] topics = [ "app/uninstalled" ] uri = "/webhooks/app/uninstalled" [[webhooks.subscriptions]] topics = [ "app/scopes_update" ] uri = "/webhooks/app/scopes_update" [access_scopes] # Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes scopes = "customer_read_orders,customer_read_customers" [auth] redirect_urls = [ "https://example.com/api/auth" ] [customer_authentication] redirect_uris = ["https:///customer-account-api/callback"] ``` ## /app/routes/customer-account-api.auth.jsx ```jsx import { redirect, useLoaderData } from "react-router"; import crypto from "crypto"; import prisma from "../db.server"; function generateCodeVerifier() { return crypto.randomBytes(32).toString("base64url"); } function generateCodeChallenge(verifier) { return crypto.createHash("sha256").update(verifier).digest("base64url"); } function generateState() { return crypto.randomBytes(16).toString("base64url"); } export const loader = async ({ request }) => { try { // Fetch OpenID configuration const openidConfigUrl = `https://${process.env.SHOP_STOREFRONT_DOMAIN}/.well-known/openid-configuration`; const openidResponse = await fetch(openidConfigUrl); if (!openidResponse.ok) { throw new Error(`Failed to fetch OpenID configuration: ${openidResponse.statusText}`); } const openidConfig = await openidResponse.json(); const authorizationEndpoint = openidConfig.authorization_endpoint; // Generate PKCE parameters const codeVerifier = generateCodeVerifier(); const codeChallenge = generateCodeChallenge(codeVerifier); const state = generateState(); // Store code verifier in database await prisma.codeVerifier.create({ data: { state, verifier: codeVerifier, }, }); // Get the callback URL from the request const url = new URL(request.url); const callbackUrl = `https://${url.host}/customer-account-api/callback`; // Get client_id from environment or config const clientId = process.env.SHOPIFY_API_KEY; // Build authorization URL const authUrl = new URL(authorizationEndpoint); authUrl.searchParams.set("client_id", clientId); authUrl.searchParams.set("response_type", "code"); authUrl.searchParams.set("redirect_uri", callbackUrl); authUrl.searchParams.set("scope", "openid email customer-account-api:full"); authUrl.searchParams.set("state", state); authUrl.searchParams.set("code_challenge", codeChallenge); authUrl.searchParams.set("code_challenge_method", "S256"); // Redirect directly to the authorization URL return redirect(authUrl.toString()); } catch (error) { console.error("Error generating auth URL:", error); // If there's an error, return error data to display return { error: error.message, }; } }; export default function CustomerAccountApiAuth() { const data = useLoaderData(); // Only renders if there's an error (otherwise loader redirects) return (

Customer Account API - Authentication Error

Error

{data?.error || "An unexpected error occurred"}

Try again
); } ``` ## /app/sessions.server.js ```javascript import { createCookieSessionStorage } from "react-router"; // Customer session storage for Customer Account API authentication // Stores customer access token ID in an encrypted, HTTP-only cookie export const customerSessionStorage = createCookieSessionStorage({ cookie: { name: "__customer_session", httpOnly: true, path: "/", sameSite: "lax", secrets: [process.env.SESSION_SECRET || "default-secret-change-in-production"], secure: process.env.NODE_ENV === "production", maxAge: 60 * 60, // 1 hour (3600 seconds) }, }); // Get customer token ID from session export async function getCustomerTokenId(request) { const session = await customerSessionStorage.getSession( request.headers.get("Cookie") ); return session.get("customerTokenId"); } // Set customer token ID in session export async function setCustomerTokenId(request, tokenId) { const session = await customerSessionStorage.getSession( request.headers.get("Cookie") ); session.set("customerTokenId", tokenId); return customerSessionStorage.commitSession(session); } // Destroy customer session export async function destroyCustomerSession(request) { const session = await customerSessionStorage.getSession( request.headers.get("Cookie") ); return customerSessionStorage.destroySession(session); } ``` ## /app/routes/customer-account-api.callback.jsx ```jsx import { redirect } from "react-router"; import { useLoaderData } from "react-router"; import prisma from "../db.server"; import { setCustomerTokenId } from "../sessions.server"; export const loader = async ({ request }) => { try { const url = new URL(request.url); const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); if (!code || !state) { throw new Error("Missing code or state parameter"); } // Retrieve the code verifier from the database const codeVerifierRecord = await prisma.codeVerifier.findUnique({ where: { state }, }); if (!codeVerifierRecord) { throw new Error("Invalid state parameter or code verifier not found"); } // Fetch OpenID configuration to get token endpoint const openidConfigUrl = `https://${process.env.SHOP_STOREFRONT_DOMAIN}/.well-known/openid-configuration`; const openidResponse = await fetch(openidConfigUrl); if (!openidResponse.ok) { throw new Error(`Failed to fetch OpenID configuration: ${openidResponse.statusText}`); } const openidConfig = await openidResponse.json(); const tokenEndpoint = openidConfig.token_endpoint; // Get callback URL const callbackUrl = `https://${url.host}/customer-account-api/callback`; // Get client_id from environment or config const clientId = process.env.SHOPIFY_API_KEY; // Exchange authorization code for access token const tokenResponse = await fetch(tokenEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ grant_type: "authorization_code", client_id: clientId, redirect_uri: callbackUrl, code: code, code_verifier: codeVerifierRecord.verifier, }), }); if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); throw new Error(`Token exchange failed: ${tokenResponse.statusText} - ${errorText}`); } const tokenData = await tokenResponse.json(); // Calculate token expiration const expiresAt = tokenData.expires_in ? new Date(Date.now() + tokenData.expires_in * 1000) : null; // Store the access token in the database const customerAccessToken = await prisma.customerAccessToken.create({ data: { shop: process.env.SHOP_STOREFRONT_DOMAIN, accessToken: tokenData.access_token, expiresAt, }, }); // Clean up the used code verifier await prisma.codeVerifier.delete({ where: { state }, }); // Store tokenId in session cookie and redirect to order list const setCookieHeader = await setCustomerTokenId(request, customerAccessToken.id); return redirect("/customer-account-api/order-list", { headers: { "Set-Cookie": setCookieHeader, }, }); } catch (error) { console.error("Error in callback:", error); return { success: false, error: error.message, }; } }; export default function CustomerAccountApiCallback() { const data = useLoaderData(); // Only renders if there's an error (otherwise loader redirects) return (

Customer Account API - Callback Error

Authentication Error

{data?.error || "An error occurred during authentication"}

Try again
); } ``` ## /app/routes/customer-account-api.order-list.jsx ```jsx import { useLoaderData } from "react-router"; import { useState, useEffect } from "react"; import prisma from "../db.server"; import { getCustomerTokenId } from "../sessions.server"; export const loader = async ({ request }) => { try { // Get tokenId from session cookie const tokenId = await getCustomerTokenId(request); if (!tokenId) { throw new Error("No customer authentication found. Please authenticate first."); } // Fetch the access token from the database const customerAccessToken = await prisma.customerAccessToken.findUnique({ where: { id: tokenId }, }); if (!customerAccessToken) { throw new Error("Access token not found"); } // Check if token is expired if (customerAccessToken.expiresAt && new Date() > customerAccessToken.expiresAt) { throw new Error("Access token has expired"); } // Get shop domain for client-side API calls const shopDomain = process.env.SHOP_STOREFRONT_DOMAIN; // Fetch Customer Account API configuration const wellKnownUrl = `https://${shopDomain}/.well-known/customer-account-api`; const wellKnownResponse = await fetch(wellKnownUrl); if (!wellKnownResponse.ok) { ``` ## Tutorial complete! Congratulations! You've built a Customer Account API authentication flow using OAuth 2.0 with PKCE. Keep the momentum going with these related tutorials and resources. ### Next steps [Customer Account API reference\ \ ](https://shopify.dev/docs/api/customer) [Explore the GraphQL Customer Account API reference.](https://shopify.dev/docs/api/customer) [Manage customer accounts\ \ ](https://shopify.dev/docs/storefronts/headless/building-with-the-customer-account-api/customer-accounts) [Learn how to manage customer accounts with the Customer Account API.](https://shopify.dev/docs/storefronts/headless/building-with-the-customer-account-api/customer-accounts) [Hydrogen's Customer Account API implementation\ \ ](https://shopify.dev/docs/api/hydrogen/latest/utilities/createcustomeraccountclient) [Explore the `createCustomerAccountClient` reference, including storefront usage examples.](https://shopify.dev/docs/api/hydrogen/latest/utilities/createcustomeraccountclient)