---
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
api_name: customer
---

# 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/stores/development-stores)

The development store should be pre-populated with [test data](https://shopify.dev/docs/apps/build/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

[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://<your-tunnel-url>"
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://<your-tunnel-url>/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=<your-ngrok-domain> 3000
```

### Add Customer Account API authentication settings

Add the `[customer_authentication]` module and `<your-tunnel-url>` 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://<your-tunnel-url>"
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://<your-tunnel-url>/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 (
    <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
      <h1>Customer Account API - Authentication Error</h1>
      <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
        <h2>Error</h2>
        <p style={{ color: "#c62828" }}>{data?.error || "An unexpected error occurred"}</p>
      </div>
      <a href="/customer-account-api/auth" style={{ marginTop: "1rem", display: "inline-block" }}>
        Try again
      </a>
    </div>
  );
}
```

### 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 (
    <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
      <h1>Customer Account API - Authentication Error</h1>
      <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
        <h2>Error</h2>
        <p style={{ color: "#c62828" }}>{data?.error || "An unexpected error occurred"}</p>
      </div>
      <a href="/customer-account-api/auth" style={{ marginTop: "1rem", display: "inline-block" }}>
        Try again
      </a>
    </div>
  );
}
```

### 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 (
    <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
      <h1>Customer Account API - Authentication Error</h1>
      <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
        <h2>Error</h2>
        <p style={{ color: "#c62828" }}>{data?.error || "An unexpected error occurred"}</p>
      </div>
      <a href="/customer-account-api/auth" style={{ marginTop: "1rem", display: "inline-block" }}>
        Try again
      </a>
    </div>
  );
}
```

### 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 (
    <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
      <h1>Customer Account API - Authentication Error</h1>
      <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
        <h2>Error</h2>
        <p style={{ color: "#c62828" }}>{data?.error || "An unexpected error occurred"}</p>
      </div>
      <a href="/customer-account-api/auth" style={{ marginTop: "1rem", display: "inline-block" }}>
        Try again
      </a>
    </div>
  );
}
```

## 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 (
    <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
      <h1>Customer Account API - Callback Error</h1>
      <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
        <h2>Authentication Error</h2>
        <p style={{ color: "#c62828" }}>{data?.error || "An error occurred during authentication"}</p>
      </div>
      <a href="/customer-account-api/auth" style={{ marginTop: "1rem", display: "inline-block", color: "#1976d2" }}>
        Try again
      </a>
    </div>
  );
}
```

### 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 (
    <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
      <h1>Customer Account API - Callback Error</h1>
      <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
        <h2>Authentication Error</h2>
        <p style={{ color: "#c62828" }}>{data?.error || "An error occurred during authentication"}</p>
      </div>
      <a href="/customer-account-api/auth" style={{ marginTop: "1rem", display: "inline-block", color: "#1976d2" }}>
        Try again
      </a>
    </div>
  );
}
```

### 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 (
    <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
      <h1>Customer Account API - Callback Error</h1>
      <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
        <h2>Authentication Error</h2>
        <p style={{ color: "#c62828" }}>{data?.error || "An error occurred during authentication"}</p>
      </div>
      <a href="/customer-account-api/auth" style={{ marginTop: "1rem", display: "inline-block", color: "#1976d2" }}>
        Try again
      </a>
    </div>
  );
}
```

### 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 (
    <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
      <h1>Customer Account API - Callback Error</h1>
      <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
        <h2>Authentication Error</h2>
        <p style={{ color: "#c62828" }}>{data?.error || "An error occurred during authentication"}</p>
      </div>
      <a href="/customer-account-api/auth" style={{ marginTop: "1rem", display: "inline-block", color: "#1976d2" }}>
        Try again
      </a>
    </div>
  );
}
```

### 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 (
    <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
      <h1>Customer Account API - Callback Error</h1>
      <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
        <h2>Authentication Error</h2>
        <p style={{ color: "#c62828" }}>{data?.error || "An error occurred during authentication"}</p>
      </div>
      <a href="/customer-account-api/auth" style={{ marginTop: "1rem", display: "inline-block", color: "#1976d2" }}>
        Try again
      </a>
    </div>
  );
}
```

### 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 (
    <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
      <h1>Customer Account API - Callback Error</h1>
      <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
        <h2>Authentication Error</h2>
        <p style={{ color: "#c62828" }}>{data?.error || "An error occurred during authentication"}</p>
      </div>
      <a href="/customer-account-api/auth" style={{ marginTop: "1rem", display: "inline-block", color: "#1976d2" }}>
        Try again
      </a>
    </div>
  );
}
```

## 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) {
      throw new Error(`Failed to fetch Customer Account API configuration: ${wellKnownResponse.statusText}`);
    }


    const wellKnownConfig = await wellKnownResponse.json();
    const graphqlApiUrl = wellKnownConfig.graphql_api;


    return {
      success: true,
      tokenId: customerAccessToken.id,
      accessToken: customerAccessToken.accessToken,
      shop: customerAccessToken.shop,
      graphqlApiUrl,
      expiresAt: customerAccessToken.expiresAt?.toISOString(),
    };
  } catch (error) {
    console.error("Error loading access token:", error);
    return {
      success: false,
      error: error.message,
    };
  }
};


export default function CustomerAccountApiOrderList() {
  const data = useLoaderData();
  const [customerData, setCustomerData] = useState(null);
  const [customerApiError, setCustomerApiError] = useState(null);
  const [isLoadingCustomerData, setIsLoadingCustomerData] = useState(false);


  // Query Customer Account API on the frontend when access token is available
  useEffect(() => {
    if (data.success && data.accessToken) {
      setIsLoadingCustomerData(true);


      const queryCustomerApi = async () => {
        try {
          const response = await fetch(data.graphqlApiUrl, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              "Authorization": data.accessToken,
            },
            body: JSON.stringify({
              query: `
                query {
                  customer {
                    id
                    emailAddress {
                      emailAddress
                    }
                    firstName
                    lastName
                    orders(first: 10) {
                      edges {
                        node {
                          id
                          name
                        }
                      }
                    }
                  }
                }
              `,
            }),
          });


          if (response.ok) {
            const apiResult = await response.json();
            if (apiResult.data?.customer) {
              console.log(apiResult.data.customer);
              setCustomerData(apiResult.data.customer);
            } else if (apiResult.errors) {
              setCustomerApiError(JSON.stringify(apiResult.errors, null, 2));
            }
          } else {
            const errorText = await response.text();
            setCustomerApiError(`API request failed: ${response.status} - ${errorText}`);
          }
        } catch (error) {
          setCustomerApiError(`Failed to query Customer Account API: ${error.message}`);
          console.error("Customer API query error:", error);
        } finally {
          setIsLoadingCustomerData(false);
        }
      };


      queryCustomerApi();
    }
  }, [data.success, data.accessToken, data.graphqlApiUrl]);


  if (!data.success) {
    return (
      <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
        <h1>Customer Account API - Order List</h1>
        <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
          <h2>Error</h2>
          <p style={{ color: "#c62828" }}>{data.error}</p>
        </div>
        <a href="/customer-account-api/auth" style={{ marginTop: "1rem", display: "inline-block" }}>
          Authenticate
        </a>
      </div>
    );
  }


  return (
    <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
      <h1>Customer Orders</h1>


      {isLoadingCustomerData && (
        <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#e3f2fd", borderRadius: "8px" }}>
          <h3>Loading Orders...</h3>
          <p style={{ fontSize: "0.875rem", color: "#666" }}>
            Fetching your order history from the Customer Account API...
          </p>
        </div>
      )}


      {customerData && (
        <div style={{ marginTop: "1rem" }}>
          <div style={{ padding: "1rem", backgroundColor: "#e8f5e9", borderRadius: "8px", marginBottom: "1rem" }}>
            <h3 style={{ marginTop: 0 }}>Customer Information</h3>
            <p style={{ margin: "0.5rem 0" }}>
              <strong>Email:</strong> {customerData.emailAddress?.emailAddress || "N/A"}
            </p>
            <p style={{ margin: "0.5rem 0" }}>
              <strong>First Name:</strong> {customerData.firstName || "N/A"}
            </p>
            <p style={{ margin: "0.5rem 0" }}>
              <strong>Last Name:</strong> {customerData.lastName || "N/A"}
            </p>
          </div>


          {customerData.orders?.edges && customerData.orders.edges.length > 0 ? (
            <div>
              <h3>Orders ({customerData.orders.edges.length})</h3>
              <ul style={{ listStyleType: "disc", paddingLeft: "1.5rem" }}>
                {customerData.orders.edges.map((orderEdge) => {
                  const order = orderEdge.node;
                  return (
                    <li key={order.id} style={{ marginBottom: "0.5rem" }}>
                      <strong>{order.name}</strong>
                    </li>
                  );
                })}
              </ul>
            </div>
          ) : (
            <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#fff9e6", borderRadius: "4px" }}>
              <em>No orders found for this customer.</em>
            </div>
          )}
        </div>
      )}


      {customerApiError && (
        <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
          <h3>❌ Customer Account API Error</h3>
          <p style={{ fontSize: "0.875rem", color: "#666", marginTop: "0.5rem" }}>
            Error occurred while querying the Customer Account API:
          </p>
          <pre style={{
            marginTop: "0.5rem",
            padding: "1rem",
            backgroundColor: "#fff",
            border: "1px solid #ef9a9a",
            borderRadius: "4px",
            fontSize: "0.75rem",
            overflow: "auto",
            whiteSpace: "pre-wrap",
            wordBreak: "break-word",
            margin: 0
          }}>
            {customerApiError}
          </pre>
        </div>
      )}
    </div>
  );
}
```

### 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) {
      throw new Error(`Failed to fetch Customer Account API configuration: ${wellKnownResponse.statusText}`);
    }


    const wellKnownConfig = await wellKnownResponse.json();
    const graphqlApiUrl = wellKnownConfig.graphql_api;


    return {
      success: true,
      tokenId: customerAccessToken.id,
      accessToken: customerAccessToken.accessToken,
      shop: customerAccessToken.shop,
      graphqlApiUrl,
      expiresAt: customerAccessToken.expiresAt?.toISOString(),
    };
  } catch (error) {
    console.error("Error loading access token:", error);
    return {
      success: false,
      error: error.message,
    };
  }
};


export default function CustomerAccountApiOrderList() {
  const data = useLoaderData();
  const [customerData, setCustomerData] = useState(null);
  const [customerApiError, setCustomerApiError] = useState(null);
  const [isLoadingCustomerData, setIsLoadingCustomerData] = useState(false);


  // Query Customer Account API on the frontend when access token is available
  useEffect(() => {
    if (data.success && data.accessToken) {
      setIsLoadingCustomerData(true);


      const queryCustomerApi = async () => {
        try {
          const response = await fetch(data.graphqlApiUrl, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              "Authorization": data.accessToken,
            },
            body: JSON.stringify({
              query: `
                query {
                  customer {
                    id
                    emailAddress {
                      emailAddress
                    }
                    firstName
                    lastName
                    orders(first: 10) {
                      edges {
                        node {
                          id
                          name
                        }
                      }
                    }
                  }
                }
              `,
            }),
          });


          if (response.ok) {
            const apiResult = await response.json();
            if (apiResult.data?.customer) {
              console.log(apiResult.data.customer);
              setCustomerData(apiResult.data.customer);
            } else if (apiResult.errors) {
              setCustomerApiError(JSON.stringify(apiResult.errors, null, 2));
            }
          } else {
            const errorText = await response.text();
            setCustomerApiError(`API request failed: ${response.status} - ${errorText}`);
          }
        } catch (error) {
          setCustomerApiError(`Failed to query Customer Account API: ${error.message}`);
          console.error("Customer API query error:", error);
        } finally {
          setIsLoadingCustomerData(false);
        }
      };


      queryCustomerApi();
    }
  }, [data.success, data.accessToken, data.graphqlApiUrl]);


  if (!data.success) {
    return (
      <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
        <h1>Customer Account API - Order List</h1>
        <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
          <h2>Error</h2>
          <p style={{ color: "#c62828" }}>{data.error}</p>
        </div>
        <a href="/customer-account-api/auth" style={{ marginTop: "1rem", display: "inline-block" }}>
          Authenticate
        </a>
      </div>
    );
  }


  return (
    <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
      <h1>Customer Orders</h1>


      {isLoadingCustomerData && (
        <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#e3f2fd", borderRadius: "8px" }}>
          <h3>Loading Orders...</h3>
          <p style={{ fontSize: "0.875rem", color: "#666" }}>
            Fetching your order history from the Customer Account API...
          </p>
        </div>
      )}


      {customerData && (
        <div style={{ marginTop: "1rem" }}>
          <div style={{ padding: "1rem", backgroundColor: "#e8f5e9", borderRadius: "8px", marginBottom: "1rem" }}>
            <h3 style={{ marginTop: 0 }}>Customer Information</h3>
            <p style={{ margin: "0.5rem 0" }}>
              <strong>Email:</strong> {customerData.emailAddress?.emailAddress || "N/A"}
            </p>
            <p style={{ margin: "0.5rem 0" }}>
              <strong>First Name:</strong> {customerData.firstName || "N/A"}
            </p>
            <p style={{ margin: "0.5rem 0" }}>
              <strong>Last Name:</strong> {customerData.lastName || "N/A"}
            </p>
          </div>


          {customerData.orders?.edges && customerData.orders.edges.length > 0 ? (
            <div>
              <h3>Orders ({customerData.orders.edges.length})</h3>
              <ul style={{ listStyleType: "disc", paddingLeft: "1.5rem" }}>
                {customerData.orders.edges.map((orderEdge) => {
                  const order = orderEdge.node;
                  return (
                    <li key={order.id} style={{ marginBottom: "0.5rem" }}>
                      <strong>{order.name}</strong>
                    </li>
                  );
                })}
              </ul>
            </div>
          ) : (
            <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#fff9e6", borderRadius: "4px" }}>
              <em>No orders found for this customer.</em>
            </div>
          )}
        </div>
      )}


      {customerApiError && (
        <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
          <h3>❌ Customer Account API Error</h3>
          <p style={{ fontSize: "0.875rem", color: "#666", marginTop: "0.5rem" }}>
            Error occurred while querying the Customer Account API:
          </p>
          <pre style={{
            marginTop: "0.5rem",
            padding: "1rem",
            backgroundColor: "#fff",
            border: "1px solid #ef9a9a",
            borderRadius: "4px",
            fontSize: "0.75rem",
            overflow: "auto",
            whiteSpace: "pre-wrap",
            wordBreak: "break-word",
            margin: 0
          }}>
            {customerApiError}
          </pre>
        </div>
      )}
    </div>
  );
}
```

### 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) {
      throw new Error(`Failed to fetch Customer Account API configuration: ${wellKnownResponse.statusText}`);
    }


    const wellKnownConfig = await wellKnownResponse.json();
    const graphqlApiUrl = wellKnownConfig.graphql_api;


    return {
      success: true,
      tokenId: customerAccessToken.id,
      accessToken: customerAccessToken.accessToken,
      shop: customerAccessToken.shop,
      graphqlApiUrl,
      expiresAt: customerAccessToken.expiresAt?.toISOString(),
    };
  } catch (error) {
    console.error("Error loading access token:", error);
    return {
      success: false,
      error: error.message,
    };
  }
};


export default function CustomerAccountApiOrderList() {
  const data = useLoaderData();
  const [customerData, setCustomerData] = useState(null);
  const [customerApiError, setCustomerApiError] = useState(null);
  const [isLoadingCustomerData, setIsLoadingCustomerData] = useState(false);


  // Query Customer Account API on the frontend when access token is available
  useEffect(() => {
    if (data.success && data.accessToken) {
      setIsLoadingCustomerData(true);


      const queryCustomerApi = async () => {
        try {
          const response = await fetch(data.graphqlApiUrl, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              "Authorization": data.accessToken,
            },
            body: JSON.stringify({
              query: `
                query {
                  customer {
                    id
                    emailAddress {
                      emailAddress
                    }
                    firstName
                    lastName
                    orders(first: 10) {
                      edges {
                        node {
                          id
                          name
                        }
                      }
                    }
                  }
                }
              `,
            }),
          });


          if (response.ok) {
            const apiResult = await response.json();
            if (apiResult.data?.customer) {
              console.log(apiResult.data.customer);
              setCustomerData(apiResult.data.customer);
            } else if (apiResult.errors) {
              setCustomerApiError(JSON.stringify(apiResult.errors, null, 2));
            }
          } else {
            const errorText = await response.text();
            setCustomerApiError(`API request failed: ${response.status} - ${errorText}`);
          }
        } catch (error) {
          setCustomerApiError(`Failed to query Customer Account API: ${error.message}`);
          console.error("Customer API query error:", error);
        } finally {
          setIsLoadingCustomerData(false);
        }
      };


      queryCustomerApi();
    }
  }, [data.success, data.accessToken, data.graphqlApiUrl]);


  if (!data.success) {
    return (
      <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
        <h1>Customer Account API - Order List</h1>
        <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
          <h2>Error</h2>
          <p style={{ color: "#c62828" }}>{data.error}</p>
        </div>
        <a href="/customer-account-api/auth" style={{ marginTop: "1rem", display: "inline-block" }}>
          Authenticate
        </a>
      </div>
    );
  }


  return (
    <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
      <h1>Customer Orders</h1>


      {isLoadingCustomerData && (
        <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#e3f2fd", borderRadius: "8px" }}>
          <h3>Loading Orders...</h3>
          <p style={{ fontSize: "0.875rem", color: "#666" }}>
            Fetching your order history from the Customer Account API...
          </p>
        </div>
      )}


      {customerData && (
        <div style={{ marginTop: "1rem" }}>
          <div style={{ padding: "1rem", backgroundColor: "#e8f5e9", borderRadius: "8px", marginBottom: "1rem" }}>
            <h3 style={{ marginTop: 0 }}>Customer Information</h3>
            <p style={{ margin: "0.5rem 0" }}>
              <strong>Email:</strong> {customerData.emailAddress?.emailAddress || "N/A"}
            </p>
            <p style={{ margin: "0.5rem 0" }}>
              <strong>First Name:</strong> {customerData.firstName || "N/A"}
            </p>
            <p style={{ margin: "0.5rem 0" }}>
              <strong>Last Name:</strong> {customerData.lastName || "N/A"}
            </p>
          </div>


          {customerData.orders?.edges && customerData.orders.edges.length > 0 ? (
            <div>
              <h3>Orders ({customerData.orders.edges.length})</h3>
              <ul style={{ listStyleType: "disc", paddingLeft: "1.5rem" }}>
                {customerData.orders.edges.map((orderEdge) => {
                  const order = orderEdge.node;
                  return (
                    <li key={order.id} style={{ marginBottom: "0.5rem" }}>
                      <strong>{order.name}</strong>
                    </li>
                  );
                })}
              </ul>
            </div>
          ) : (
            <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#fff9e6", borderRadius: "4px" }}>
              <em>No orders found for this customer.</em>
            </div>
          )}
        </div>
      )}


      {customerApiError && (
        <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
          <h3>❌ Customer Account API Error</h3>
          <p style={{ fontSize: "0.875rem", color: "#666", marginTop: "0.5rem" }}>
            Error occurred while querying the Customer Account API:
          </p>
          <pre style={{
            marginTop: "0.5rem",
            padding: "1rem",
            backgroundColor: "#fff",
            border: "1px solid #ef9a9a",
            borderRadius: "4px",
            fontSize: "0.75rem",
            overflow: "auto",
            whiteSpace: "pre-wrap",
            wordBreak: "break-word",
            margin: 0
          }}>
            {customerApiError}
          </pre>
        </div>
      )}
    </div>
  );
}
```

### 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) {
      throw new Error(`Failed to fetch Customer Account API configuration: ${wellKnownResponse.statusText}`);
    }


    const wellKnownConfig = await wellKnownResponse.json();
    const graphqlApiUrl = wellKnownConfig.graphql_api;


    return {
      success: true,
      tokenId: customerAccessToken.id,
      accessToken: customerAccessToken.accessToken,
      shop: customerAccessToken.shop,
      graphqlApiUrl,
      expiresAt: customerAccessToken.expiresAt?.toISOString(),
    };
  } catch (error) {
    console.error("Error loading access token:", error);
    return {
      success: false,
      error: error.message,
    };
  }
};


export default function CustomerAccountApiOrderList() {
  const data = useLoaderData();
  const [customerData, setCustomerData] = useState(null);
  const [customerApiError, setCustomerApiError] = useState(null);
  const [isLoadingCustomerData, setIsLoadingCustomerData] = useState(false);


  // Query Customer Account API on the frontend when access token is available
  useEffect(() => {
    if (data.success && data.accessToken) {
      setIsLoadingCustomerData(true);


      const queryCustomerApi = async () => {
        try {
          const response = await fetch(data.graphqlApiUrl, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              "Authorization": data.accessToken,
            },
            body: JSON.stringify({
              query: `
                query {
                  customer {
                    id
                    emailAddress {
                      emailAddress
                    }
                    firstName
                    lastName
                    orders(first: 10) {
                      edges {
                        node {
                          id
                          name
                        }
                      }
                    }
                  }
                }
              `,
            }),
          });


          if (response.ok) {
            const apiResult = await response.json();
            if (apiResult.data?.customer) {
              console.log(apiResult.data.customer);
              setCustomerData(apiResult.data.customer);
            } else if (apiResult.errors) {
              setCustomerApiError(JSON.stringify(apiResult.errors, null, 2));
            }
          } else {
            const errorText = await response.text();
            setCustomerApiError(`API request failed: ${response.status} - ${errorText}`);
          }
        } catch (error) {
          setCustomerApiError(`Failed to query Customer Account API: ${error.message}`);
          console.error("Customer API query error:", error);
        } finally {
          setIsLoadingCustomerData(false);
        }
      };


      queryCustomerApi();
    }
  }, [data.success, data.accessToken, data.graphqlApiUrl]);


  if (!data.success) {
    return (
      <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
        <h1>Customer Account API - Order List</h1>
        <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
          <h2>Error</h2>
          <p style={{ color: "#c62828" }}>{data.error}</p>
        </div>
        <a href="/customer-account-api/auth" style={{ marginTop: "1rem", display: "inline-block" }}>
          Authenticate
        </a>
      </div>
    );
  }


  return (
    <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
      <h1>Customer Orders</h1>


      {isLoadingCustomerData && (
        <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#e3f2fd", borderRadius: "8px" }}>
          <h3>Loading Orders...</h3>
          <p style={{ fontSize: "0.875rem", color: "#666" }}>
            Fetching your order history from the Customer Account API...
          </p>
        </div>
      )}


      {customerData && (
        <div style={{ marginTop: "1rem" }}>
          <div style={{ padding: "1rem", backgroundColor: "#e8f5e9", borderRadius: "8px", marginBottom: "1rem" }}>
            <h3 style={{ marginTop: 0 }}>Customer Information</h3>
            <p style={{ margin: "0.5rem 0" }}>
              <strong>Email:</strong> {customerData.emailAddress?.emailAddress || "N/A"}
            </p>
            <p style={{ margin: "0.5rem 0" }}>
              <strong>First Name:</strong> {customerData.firstName || "N/A"}
            </p>
            <p style={{ margin: "0.5rem 0" }}>
              <strong>Last Name:</strong> {customerData.lastName || "N/A"}
            </p>
          </div>


          {customerData.orders?.edges && customerData.orders.edges.length > 0 ? (
            <div>
              <h3>Orders ({customerData.orders.edges.length})</h3>
              <ul style={{ listStyleType: "disc", paddingLeft: "1.5rem" }}>
                {customerData.orders.edges.map((orderEdge) => {
                  const order = orderEdge.node;
                  return (
                    <li key={order.id} style={{ marginBottom: "0.5rem" }}>
                      <strong>{order.name}</strong>
                    </li>
                  );
                })}
              </ul>
            </div>
          ) : (
            <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#fff9e6", borderRadius: "4px" }}>
              <em>No orders found for this customer.</em>
            </div>
          )}
        </div>
      )}


      {customerApiError && (
        <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
          <h3>❌ Customer Account API Error</h3>
          <p style={{ fontSize: "0.875rem", color: "#666", marginTop: "0.5rem" }}>
            Error occurred while querying the Customer Account API:
          </p>
          <pre style={{
            marginTop: "0.5rem",
            padding: "1rem",
            backgroundColor: "#fff",
            border: "1px solid #ef9a9a",
            borderRadius: "4px",
            fontSize: "0.75rem",
            overflow: "auto",
            whiteSpace: "pre-wrap",
            wordBreak: "break-word",
            margin: 0
          }}>
            {customerApiError}
          </pre>
        </div>
      )}
    </div>
  );
}
```

## 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://<your-tunnel-url>: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://<your-tunnel-url>/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.

##### 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://<your-tunnel-url>"
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://<your-tunnel-url>/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 (
    <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
      <h1>Customer Account API - Authentication Error</h1>
      <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
        <h2>Error</h2>
        <p style={{ color: "#c62828" }}>{data?.error || "An unexpected error occurred"}</p>
      </div>
      <a href="/customer-account-api/auth" style={{ marginTop: "1rem", display: "inline-block" }}>
        Try again
      </a>
    </div>
  );
}
```

## /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 (
    <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
      <h1>Customer Account API - Callback Error</h1>
      <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
        <h2>Authentication Error</h2>
        <p style={{ color: "#c62828" }}>{data?.error || "An error occurred during authentication"}</p>
      </div>
      <a href="/customer-account-api/auth" style={{ marginTop: "1rem", display: "inline-block", color: "#1976d2" }}>
        Try again
      </a>
    </div>
  );
}
```

## /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) {
      throw new Error(`Failed to fetch Customer Account API configuration: ${wellKnownResponse.statusText}`);
    }


    const wellKnownConfig = await wellKnownResponse.json();
    const graphqlApiUrl = wellKnownConfig.graphql_api;


    return {
      success: true,
      tokenId: customerAccessToken.id,
      accessToken: customerAccessToken.accessToken,
      shop: customerAccessToken.shop,
      graphqlApiUrl,
      expiresAt: customerAccessToken.expiresAt?.toISOString(),
    };
  } catch (error) {
    console.error("Error loading access token:", error);
    return {
      success: false,
      error: error.message,
    };
  }
};


export default function CustomerAccountApiOrderList() {
  const data = useLoaderData();
  const [customerData, setCustomerData] = useState(null);
  const [customerApiError, setCustomerApiError] = useState(null);
  const [isLoadingCustomerData, setIsLoadingCustomerData] = useState(false);


  // Query Customer Account API on the frontend when access token is available
  useEffect(() => {
    if (data.success && data.accessToken) {
      setIsLoadingCustomerData(true);


      const queryCustomerApi = async () => {
        try {
          const response = await fetch(data.graphqlApiUrl, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              "Authorization": data.accessToken,
            },
            body: JSON.stringify({
              query: `
                query {
                  customer {
                    id
                    emailAddress {
                      emailAddress
                    }
                    firstName
                    lastName
                    orders(first: 10) {
                      edges {
                        node {
                          id
                          name
                        }
                      }
                    }
                  }
                }
              `,
            }),
          });


          if (response.ok) {
            const apiResult = await response.json();
            if (apiResult.data?.customer) {
              console.log(apiResult.data.customer);
              setCustomerData(apiResult.data.customer);
            } else if (apiResult.errors) {
              setCustomerApiError(JSON.stringify(apiResult.errors, null, 2));
            }
          } else {
            const errorText = await response.text();
            setCustomerApiError(`API request failed: ${response.status} - ${errorText}`);
          }
        } catch (error) {
          setCustomerApiError(`Failed to query Customer Account API: ${error.message}`);
          console.error("Customer API query error:", error);
        } finally {
          setIsLoadingCustomerData(false);
        }
      };


      queryCustomerApi();
    }
  }, [data.success, data.accessToken, data.graphqlApiUrl]);


  if (!data.success) {
    return (
      <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
        <h1>Customer Account API - Order List</h1>
        <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
          <h2>Error</h2>
          <p style={{ color: "#c62828" }}>{data.error}</p>
        </div>
        <a href="/customer-account-api/auth" style={{ marginTop: "1rem", display: "inline-block" }}>
          Authenticate
        </a>
      </div>
    );
  }


  return (
    <div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
      <h1>Customer Orders</h1>


      {isLoadingCustomerData && (
        <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#e3f2fd", borderRadius: "8px" }}>
          <h3>Loading Orders...</h3>
          <p style={{ fontSize: "0.875rem", color: "#666" }}>
            Fetching your order history from the Customer Account API...
          </p>
        </div>
      )}


      {customerData && (
        <div style={{ marginTop: "1rem" }}>
          <div style={{ padding: "1rem", backgroundColor: "#e8f5e9", borderRadius: "8px", marginBottom: "1rem" }}>
            <h3 style={{ marginTop: 0 }}>Customer Information</h3>
            <p style={{ margin: "0.5rem 0" }}>
              <strong>Email:</strong> {customerData.emailAddress?.emailAddress || "N/A"}
            </p>
            <p style={{ margin: "0.5rem 0" }}>
              <strong>First Name:</strong> {customerData.firstName || "N/A"}
            </p>
            <p style={{ margin: "0.5rem 0" }}>
              <strong>Last Name:</strong> {customerData.lastName || "N/A"}
            </p>
          </div>


          {customerData.orders?.edges && customerData.orders.edges.length > 0 ? (
            <div>
              <h3>Orders ({customerData.orders.edges.length})</h3>
              <ul style={{ listStyleType: "disc", paddingLeft: "1.5rem" }}>
                {customerData.orders.edges.map((orderEdge) => {
                  const order = orderEdge.node;
                  return (
                    <li key={order.id} style={{ marginBottom: "0.5rem" }}>
                      <strong>{order.name}</strong>
                    </li>
                  );
                })}
              </ul>
            </div>
          ) : (
            <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#fff9e6", borderRadius: "4px" }}>
              <em>No orders found for this customer.</em>
            </div>
          )}
        </div>
      )}


      {customerApiError && (
        <div style={{ marginTop: "1rem", padding: "1rem", backgroundColor: "#ffebee", borderRadius: "8px" }}>
          <h3>❌ Customer Account API Error</h3>
          <p style={{ fontSize: "0.875rem", color: "#666", marginTop: "0.5rem" }}>
            Error occurred while querying the Customer Account API:
          </p>
          <pre style={{
            marginTop: "0.5rem",
            padding: "1rem",
            backgroundColor: "#fff",
            border: "1px solid #ef9a9a",
            borderRadius: "4px",
            fontSize: "0.75rem",
            overflow: "auto",
            whiteSpace: "pre-wrap",
            wordBreak: "break-word",
            margin: 0
          }}>
            {customerApiError}
          </pre>
        </div>
      )}
    </div>
  );
}
```

## 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/2026-10/customer-accounts/create-customer-account-client)

[Explore the `createCustomerAccountClient` reference, including storefront usage examples.](https://shopify.dev/docs/api/hydrogen/2026-10/customer-accounts/create-customer-account-client)
