My App
Auth

Merge Guest Data

Transfer guest cart and wishlist to authenticated user account

Merge Guest Data

When a guest user signs up or signs in, merge their cart and wishlist items into their authenticated account.

This endpoint should be called immediately after successful sign-up or sign-in if a guest token exists. It ensures users don't lose items they added while browsing as a guest.

Endpoint

POST /v1/auth/merge-guest-data

Request Headers

HeaderTypeRequiredDescription
x-api-keystringYesYour API key
x-customer-idstringYesAuthenticated user's ID
Content-TypestringYesMust be application/json

Request Body

FieldTypeRequiredDescription
guestTokenstringYesThe guest token from previous session

Response

Success Response (200)

{
  "success": true,
  "message": "Guest data merged successfully",
  "merged": {
    "cartItems": 3,
    "wishlistItems": 2
  }
}

Error Responses

CodeDescription
400Invalid guest token
401User must be authenticated (missing x-customer-id)
500Failed to merge guest data

How It Works

  1. Guest Browsing: User adds items to cart/wishlist without logging in
  2. Sign Up/In: User creates account or logs in
  3. Auto Merge: System transfers guest items to user's account
  4. Clean Up: Guest token is removed
sequenceDiagram
    participant Guest
    participant App
    participant API
    
    Guest->>App: Add items to cart (as guest)
    App->>API: POST /v1/cart (with x-guest-token)
    API-->>App: Returns guest_token in header
    App->>App: Store guest_token in cookie
    
    Guest->>App: Sign up
    App->>API: POST /v1/signup
    API-->>App: Returns user data & auth token
    
    App->>API: POST /v1/auth/merge-guest-data
    Note over API: Merges cart & wishlist<br/>to user account
    API-->>App: Success
    App->>App: Delete guest_token cookie

Examples

interface MergeGuestDataRequest {
  guestToken: string;
}

interface MergeGuestDataResponse {
  success: boolean;
  message: string;
  merged: {
    cartItems: number;
    wishlistItems: number;
  };
}

async function mergeGuestData(
  guestToken: string,
  userId: string
): Promise<MergeGuestDataResponse> {
  const response = await fetch(
    'https://api.yourstore.com/v1/auth/merge-guest-data',
    {
      method: 'POST',
      headers: {
        'x-api-key': process.env.BACKEND_API_KEY!,
        'x-customer-id': userId,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ guestToken }),
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Failed to merge guest data');
  }

  return response.json();
}

// Usage
try {
  const result = await mergeGuestData('guest_token_123', 'user_456');
  console.log(`Merged ${result.merged.cartItems} cart items`);
  console.log(`Merged ${result.merged.wishlistItems} wishlist items`);
} catch (error) {
  console.error('Merge failed:', error);
  // Continue anyway - user is still logged in
}
async function mergeGuestData(guestToken, userId) {
  const response = await fetch(
    'https://api.yourstore.com/v1/auth/merge-guest-data',
    {
      method: 'POST',
      headers: {
        'x-api-key': process.env.BACKEND_API_KEY,
        'x-customer-id': userId,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ guestToken }),
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Failed to merge guest data');
  }

  return response.json();
}

// Usage
mergeGuestData('guest_token_123', 'user_456')
  .then(result => {
    console.log(`Merged ${result.merged.cartItems} cart items`);
    console.log(`Merged ${result.merged.wishlistItems} wishlist items`);
  })
  .catch(error => {
    console.error('Merge failed:', error);
  });
'use server'

import { cookies } from 'next/headers';

export async function signInWithGuestMerge(email: string, password: string) {
  // 1. Get guest token before signing in
  const cookieStore = await cookies();
  const guestToken = cookieStore.get('guest_token')?.value;

  // 2. Sign in
  const signInResponse = await fetch('https://api.yourstore.com/v1/signin', {
    method: 'POST',
    headers: {
      'x-api-key': process.env.BACKEND_API_KEY!,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email, password }),
  });

  if (!signInResponse.ok) {
    throw new Error('Sign in failed');
  }

  const { token, user } = await signInResponse.json();

  // 3. Store auth data
  cookieStore.set('auth_token', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 90,
  });

  cookieStore.set('user_data', JSON.stringify(user), {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 90,
  });

  // 4. Merge guest data if token exists
  if (guestToken) {
    try {
      const mergeResponse = await fetch(
        'https://api.yourstore.com/v1/auth/merge-guest-data',
        {
          method: 'POST',
          headers: {
            'x-api-key': process.env.BACKEND_API_KEY!,
            'x-customer-id': user.id,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ guestToken }),
        }
      );

      if (mergeResponse.ok) {
        const mergeData = await mergeResponse.json();
        console.log('Guest data merged:', mergeData.merged);
      }
    } catch (error) {
      console.error('Failed to merge guest data:', error);
      // Don't fail the sign-in if merge fails
    } finally {
      // 5. Clear guest token
      cookieStore.delete('guest_token');
    }
  }

  return { user };
}

The CookieManager utility simplifies this process:

'use server'

import { cookieManager } from '@/lib/auth-tools';

export async function signInWithAutoMerge(email: string, password: string) {
  // Get guest token before it's cleared
  const guestToken = await cookieManager.getGuestToken();

  // Sign in
  const response = await fetch('https://api.yourstore.com/v1/signin', {
    method: 'POST',
    headers: await cookieManager.buildApiHeaders(),
    body: JSON.stringify({ email, password }),
  });

  if (!response.ok) throw new Error('Sign in failed');

  const loginResponse = await response.json();
  
  // This automatically clears guest token
  await cookieManager.setAuthenticatedUser(loginResponse);

  // Merge if guest token existed
  if (guestToken) {
    await mergeGuestDataHelper(guestToken, loginResponse.user.id);
  }

  return loginResponse.user;
}

async function mergeGuestDataHelper(guestToken: string, userId: string) {
  try {
    const response = await fetch(
      'https://api.yourstore.com/v1/auth/merge-guest-data',
      {
        method: 'POST',
        headers: {
          'x-api-key': process.env.BACKEND_API_KEY!,
          'x-customer-id': userId,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ guestToken }),
      }
    );

    if (!response.ok) {
      console.error('Merge failed but continuing...');
    }
  } catch (error) {
    console.error('Error merging guest data:', error);
    // Don't throw - user is still signed in
  }
}

UI Feedback

Show users when their items are being merged:

'use client'

import { useState } from 'react';

export default function SignInForm() {
  const [merging, setMerging] = useState(false);
  const [mergeResult, setMergeResult] = useState<{
    cartItems: number;
    wishlistItems: number;
  } | null>(null);

  async function handleSignIn(formData: FormData) {
    setMerging(true);
    
    const result = await signInAction(formData);
    
    if (result.merged) {
      setMergeResult(result.merged);
      setTimeout(() => setMergeResult(null), 3000);
    }
    
    setMerging(false);
  }

  return (
    <>
      {merging && (
        <div className="alert alert-info">
          Transferring your cart items...
        </div>
      )}
      
      {mergeResult && (
        <div className="alert alert-success">
Transferred {mergeResult.cartItems} cart items and{' '}
          {mergeResult.wishlistItems} wishlist items to your account!
        </div>
      )}
      
      <form action={handleSignIn}>
        {/* form fields */}
      </form>
    </>
  );
}

Handling Conflicts

If both guest and authenticated user have the same product in cart, the API typically:

  • Combines quantities
  • Or keeps the higher quantity
  • Check your backend implementation for specific behavior

Best Practices

Always Attempt Merge

Even if you're not sure a guest token exists, attempt the merge:

async function afterAuthentication(user: User) {
  const guestToken = await cookieManager.getGuestToken();
  
  if (guestToken) {
    // Fire and forget - don't block user flow
    mergeGuestData(guestToken, user.id).catch(console.error);
  }
  
  // Continue with redirect
  redirect('/dashboard');
}

Silent Failure

Don't block authentication flow if merge fails:

try {
  await mergeGuestData(guestToken, userId);
} catch (error) {
  // Log error but don't show to user
  console.error('Guest data merge failed:', error);
  // User is still successfully authenticated
}

Clear Token After Merge

Always clear the guest token after successful merge:

await cookieManager.clearGuestToken();

Common Issues

Issue: "Invalid guest token"

Cause: Token expired or already used

Solution:

if (response.status === 400) {
  // Token already merged or invalid - safe to continue
  await cookieManager.clearGuestToken();
  return { success: true, merged: { cartItems: 0, wishlistItems: 0 } };
}

Issue: "User must be authenticated"

Cause: Missing x-customer-id header

Solution: Ensure user data is stored before calling merge:

await cookieManager.setAuthenticatedUser(loginResponse);
// Now guest merge will have access to customer ID
await mergeGuestData(guestToken);

Testing the Flow

Test both scenarios in your application:

// Test 1: Guest with items signs up
async function testGuestSignup() {
  // 1. Add items as guest
  await addToCart(productId);
  const guestToken = await cookieManager.getGuestToken();
  
  // 2. Sign up
  await signUp({ name, email, password });
  
  // 3. Verify items are in authenticated cart
  const cart = await getCart();
  expect(cart.items.length).toBeGreaterThan(0);
}

// Test 2: User with empty guest session
async function testEmptyGuestSignup() {
  // Sign up without adding anything
  await signUp({ name, email, password });
  
  // Should not error
  expect(response.ok).toBe(true);
}