Register Customer with Email Verification in Storefront

In this guide, you'll learn how to implement a customer registration flow with email verification in your storefront.

Note: If you don't have email verification enabled in the Medusa application, follow the Register Customer in Storefront guide instead.
Note: This guide has been updated since Medusa v2.16.0, which made changes to the overall verification flow.

Register Customer with Verification Flow#

By default, Medusa doesn't require email verification for customers. When email verification is enabled, the registration flow is as follows:

  1. Show the customer a form to enter their details.
  2. Send a POST request to the /auth/customer/emailpass/register Get Registration Token API route to obtain a registration JWT token.
  3. Request returns a registration JWT token if the registration is successful.
  4. Redirect customer to login.
  5. Send a POST request to the /auth/customer/emailpass Login Customer API route, passing the registration JWT token in the header.
  6. Request returns an object indicating that verification is required.
  7. Send a POST request to the /auth/verification/request Request Verification Email API route to send the verification email to the customer.
  8. Customer receives the email and clicks the verification link, which is a page in the storefront.
  9. The verification page sends a POST request to the /auth/verification/confirm Confirm Verification API route to verify the customer's email using the token query parameter in the URL.
  10. Once the email is verified, the customer can log in.
  11. During login, if the email is verified but the customer's account is not active, send a request to the Register Customer API route passing the login JWT token in the header.
  12. Login the customer again to obtain an active JWT token.

This guide will cover how to:

  1. Create a registration page with email verification.
  2. Handle the login flow for customers that need to verify their email.
  3. Create a verification page that the customer receives in the email.

Step 1: Create the Registration Page#

The registration page implements steps one to four of the email verification flow. The page shows a form to collect the customer's details, sends a request to the registration API route to obtain a registration token, then redirects the customer to the login page.

The following example shows the updated registration page:

In the above example, you implement the registration flow as follows:

  • Obtain a registration JWT token from the /auth/customer/emailpass/register API route using the auth.register method. The token isn't usable until the customer verifies their email, which the login page handles.
  • Persist the customer's first and last name in localStorage so that the login page can use them to create the customer after verification.
  • If an error is thrown:
    • If the error is an existing identity error, the customer can still continue to the login page to link a customer record. This is useful when another identity, such as an admin user, exists with the same email.
    • For other errors, show an alert and exit execution.
  • After registering, redirect the customer to the login page to continue the registration flow.

The JS SDK automatically stores and re-uses the authentication headers or session in the auth.register and auth.login methods. So, if you're not using the JS SDK, make sure to pass the received authentication tokens as explained in the API reference.


Step 2: Create the Login Page#

The login page implements steps five to twelve of the email verification flow. When the customer logs in, if verification is required, request the verification email and prompt the customer to check their inbox.

After the customer verifies their email and logs in again, if the email is verified but the customer's account is not active, send a request to the Register Customer API route passing the login JWT token in the header, then log in the customer again to obtain an active JWT token.

The following example shows the updated login page:

In the example above, you:

  1. Create a handleLogin function that logs in a customer.
  2. In the function, you log in the customer using the sdk.auth.login method.
    • If the response indicates that verification is required, resend the verification email and show a "check your email" message.
    • If an error occurs, show an alert and exit execution.
    • The method may return an object with a location property. This occurs when using third-party authentication providers. Learn more about implementing third-party authentication in the Third-Party Login guide.
    • If the response is a token string, decode it to check if the actor_id is present.
      • If it's not present, it means that the customer record hasn't been created yet, so you create it using the registration token, then log in again to mint a token with the populated actor_id.
  3. If no errors occur, all subsequent requests are now authenticated. As an example, you send a request to obtain the logged-in customer's details.
Why re-login instead of `sdk.auth.refresh`? The post-register token's actor_id is empty, and sdk.auth.refresh can return another empty-actor_id token or fail. A fresh login after the customer is linked is guaranteed to produce a valid token.

Persisting Extra Signup Fields#

Because customer creation is deferred to first login, any extra fields you collect at registration (first_name, last_name, phone, and so on) need to survive until then. You have two options:

  • Option A (simplest): Only collect email and password at signup. Collect the rest on a "complete your profile" page after first login.
  • Option B: Persist the extra fields temporarily and read them in the login flow before calling customer.create. The example in this guide uses Option B with localStorage, which works across tabs.

If you keep the extra fields in component state only, they are lost the moment the customer closes the tab to open their email. Persist to localStorage (or a similar storage) before showing the pending-verification UI.

Handle Verification with Server Actions and Session Authentication#

If you implement the login flow in a server action using session-based authentication, the session cookie set by the /auth/session API route isn't persisted between calls in the same server action. So, when the login response indicates that verification is required, you must pass the token returned in that response manually as a bearer token in the Authorization header when requesting the verification email.

For example, where token is the response of the sdk.auth.login method:

Code
1if (typeof token === "object" && token.verification_required) {2  // In a server action there's no cookie jar, so a3  // session cookie set by /auth/session wouldn't carry4  // over to the next request. Pass the actorless token5  // returned from login directly as a bearer token to6  // authenticate the verification request instead.7  await sdk.client.fetch("/auth/verification/request", {8    method: "POST",9    headers: {10      Authorization: `Bearer ${token.token}`,11    },12    body: {13      entity_id: email,14      entity_type: "email",15      code_provider: "token",16    },17  })18
19  return { verification_required: true }20}

This guide's examples are otherwise client-side and rely on localStorage and the JS SDK's automatic token storage. If your storefront uses session-based authentication (like the official Next.js starter), make sure the session cookie is persisted between requests. The bearer-token caveat above applies anywhere you request a verification email, including the resend flow that runs after an unverified login.


Step 3: Create the Verify Account Page#

Next, create a page in your storefront at the path that the verification link points to (for example, /verify-account). The page reads the token from the query string and sends it to the Confirm Verification API route to verify the customer's email.

For example, create the page at src/app/verify-account/page.tsx with the following content:

You add handling for three states in the verification page:

  • verifying: The page is verifying the token by sending a request to the Medusa application.
  • success: The token is verified. The customer can now log in.
  • error: The token is invalid or expired. The customer is directed back to the login page to receive a new verification email.

When the page loads, the useEffect hook reads the token from the query string and sends it to the sdk.auth.verification.confirm method. If the request succeeds, the state is set to success. Otherwise, it's set to error.

Was this page helpful?
Ask Bloom
For assistance in your development, use Claude Code Plugins or Medusa MCP server in Cursor, VSCode, etc...FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break