Skip to main content

Anonymous Cart Checkout

This documentation describes the anonymous cart checkout flow: how the system allows customers who are not logged in to complete checkout by resolving their identity at payment time. This document describes how that association works and how the flow is split between registered and unregistered users.
Anonymous cart checkout lets a guest user add items to a cart (without logging in) and complete payment. The cart is tied to a session or device (e.g., via cart_id) rather than to a user account. The cart is identified by a cart identifier** (e.g. cart_id) that is not tied to a user account. At checkout time, the backend must associate the order with a customer so that the platform can create the order and fulfill it. To create an order on the platform, the backend must still associate the transaction with a user. The backend treats it as “anonymous” when a flag such as is_anonymous_cart is set.

That association is done in one of two ways:

  1. Registered customer (not logged in): The customer has an account (e.g. registered earlier with the same phone/email) but is currently not logged in. The system searches for an existing user (e.g. by phone) and links the order to that customer. The customer already has an account (e.g. registered previously with the same phone number). The backend searches for that customer (e.g. via a “search user” API using phone) and uses the returned user_id.

Registered + not logged inSearch user API → use existing user_id

  1. Unregistered user: The customer has no account. The system creates a new customer (e.g. with phone and name from checkout) and links the order to that new user. The customer has no account. The backend creates a new user (e.g. via a “create user” API with phone, name, etc. from checkout) and uses the returned user_id.

UnregisteredCreate user API → use new user_id

Logged-in users

If the user is logged in, the frontend sends user_id (and optionally user_email, user_phone, user_name) and the backend uses that directly. No search or create user step is needed for anonymous resolution.

High-level Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│ 1. FRONTEND │
│ User adds items (not logged in) → cart stored with cart_id │
│ User proceeds to checkout → frontend sends: cart_id, is_anonymous_cart │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 2. ORDER CREATION │
│ Backend creates payment order; stores order with meta: │
│ cart_id, is_anonymous_cart, user_id (if logged in) │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 3. USER PAYS │
│ Payment gateway collects payment; on success, backend is invoked │
│ (e.g. callback or webhook) with payment + customer details │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 4. ANONYMOUS USER RESOLUTION (only if is_anonymous_cart) │
│ • Extract phone (and optionally name) from customer details │
│ • Search user by phone → if found: use that user_id (REGISTERED) │
│ • If not found: create user with phone/name → use new user_id (UNREGISTERED)│
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 5. PLATFORM CHECKOUT │
│ Call platform “checkout cart” API with: cart_id, user_id, │
│ addresses, payment info, and header e.g. x-anonymous-cart: true │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 6. POST-PAYMENT │
│ On success: optionally clear/delete anonymous cart by cart_id │
└─────────────────────────────────────────────────────────────────────────────┘

Phone number is typically required for anonymous checkout so that the backend can either find or create a user. If phone is missing, the flow should fail with a clear error.

What Gets Stored in the Order

The order record (or “order meta”) should persist at least:

  • cart_id: the same cart can be used for platform checkout and for post-payment cart deletion.
  • is_anonymous_cart: the backend knows to run user resolution and to send anonymous-cart headers and to clear the cart after payment.
  • user_id: set from session (logged in) or from search/create user step (anonymous).

These are used later when the payment gateway calls back or sends a webhook, so that the same order can be completed on the platform and the anonymous cart can be cleaned up.

Frontend Detection of Anonymous Cart

This section describes how the frontend detects that the current cart is an anonymous cart (user not logged in) and what it sends to the backend so that the order creation and user-resolution flow can run correctly.

The backend needs to know:

  1. Whether the cart is anonymous (is_anonymous_cart).
  2. The cart identifier (cart_id) so it can fetch the cart and later perform checkout and optional cart deletion.
  3. For logged-in users: user_id

The frontend derives these from app state (e.g. auth + cart) and sends them in the order creation request (and, if applicable, in add-to-cart or buy-now requests).

Detecting anonymous cart

// Assume you have a global store or auth context
const state = getAppState(); // e.g. window.store.getState() or React context

const isLoggedIn = state?.auth?.logged_in ?? false;
const isAnonymousCart = !isLoggedIn;

const userId = state?.auth?.user_data?.id ?? null;
const cartId = state?.cart?.cart_items?.id ?? state?.cart?.id ?? null;

Use whatever your app uses for “logged in” (e.g. auth.logged_in, presence of a token, or user_data). The important part is: if the user is not logged in, set is_anonymous_cart: true in the request body.

What to send in the order creation request

When the user clicks “Checkout” or “Proceed to payment”, the frontend should call the order creation API with at least:

FieldRequiredDescription
cart_idYes (for cart checkout)Cart identifier from app state.
is_anonymous_cartYestrue if user is not logged in, else false.
user_idIf logged inUser ID from auth state; omit for anonymous.
user_emailOptionalFor prefill when logged in.
user_phoneOptionalFor prefill when logged in.
user_nameOptionalFor prefill when logged in.
const requestBody = {
...(cartId ? { cart_id: cartId } : {}),
is_anonymous_cart: isAnonymousCart,
...(userId ? { user_id: userId } : {}),
...(isLoggedIn && userEmail ? { user_email: userEmail } : {}),
...(isLoggedIn && userPhone ? { user_phone: userPhone } : {}),
...(isLoggedIn && userName ? { user_name: userName } : {}),
// ... other fields your API needs (e.g. coupon_data)
};

const response = await fetch('/api/checkout/order', {
method: 'POST',
headers: { 'Content-Type': 'application/json', /* ... */ },
credentials: 'include',
body: JSON.stringify(requestBody),
});
  • For anonymous users: do not send user_id; the backend will resolve it via search/create user using phone from payment/checkout details.
  • For logged-in users: send user_id (and optionally prefill fields) so the backend does not run anonymous user resolution.

Add-to-cart / Buy-now requests

If your flow has a separate “add to cart” or “buy now” endpoint that eventually leads to the same checkout, that request should also indicate anonymous cart so the backend can:

  • Associate the cart with the correct session or anonymous context.
  • Store the same is_anonymous_cart flag when creating the payment order later.

Example

const addToCartPayload = {
// ... product/variant/quantity fields
is_anonymous_cart: isAnonymousCart,
...(userId ? { user_id: userId } : {}),
};

await fetch('/api/cart/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(addToCartPayload),
});

Exact field names and endpoints depend on your API contract.

Where to read cart_id and auth state

  • Cart ID: Usually from the cart slice of your global state (e.g. after cart fetch or add-to-cart). For example, the ID returned by the cart API.
  • Auth: From auth slice or context: state.auth.logged_in, state.auth.user_data.id, and for prefill: user_data.emails, user_data.phone_numbers, user_data.first_name, user_data.last_name.

Ensure the cart ID you send is the one the backend will use for:

  • Fetching cart details at order creation.
  • Calling the platform “checkout cart” API.
  • Deleting the cart after successful payment (if applicable).

Order Creation and Storing Anonymous Cart Flag

This document describes how the order creation API accepts the frontend payload (including is_anonymous_cart and cart_id), fetches cart data, creates a payment order, and persists the anonymous cart flag and related data in the order record. The backend uses this stored meta later when the payment gateway calls back or sends a webhook, to run user resolution and platform checkout.

Example

// Generic: read from request body
const cartId = req.body?.cart_id ?? null;
const userId = req.body?.user_id ?? null;
const isAnonymousCart = Boolean(req.body?.is_anonymous_cart);

The order creation endpoint typically receives a POST body with at least:

FieldTypeDescription
cart_idstringCart identifier from the frontend (required for cart checkout).
is_anonymous_cartbooleantrue if the user is not logged in; otherwise false.
user_idstringnull

Fetching cart data

The backend must fetch the cart using cart_id so it can compute totals, line items, and validate the cart. For anonymous carts, the cart API might require a special header (e.g. x-anonymous-cart: true) or no auth, so that the cart is resolved by cart_id only and not by a logged-in user.

Example

// Generic: fetch cart; pass anonymous flag so the right headers/context are used
const cartData = await getCartData(req, cartId, isAnonymousCart);

if (!cartData?.cart) {
return res.status(400).json({ success: false, message: 'Unable to fetch cart' });
}

const cart = cartData.cart;

Implementation of getCartData should:

  • Use cart_id in the cart/GraphQL request when provided.
  • When isAnonymousCart is true, either send the platform’s anonymous-cart header or omit auth so the cart is fetched by ID only (behavior is platform-specific).

Example

// Generic: build order meta for persistence
const orderMeta = {
cart_id: cartId ?? cart.cart_id,
is_anonymous_cart: isAnonymousCart,
user_id: userId, // null for anonymous; set later during user resolution
company_id: companyId,
application_id: applicationId,
// ... other fields (line_items, breakup, payment order id, etc.)
};

When the payment gateway later invokes your callback or webhook, you load this order by payment order ID or receipt, read is_anonymous_cart and cart_id, and then run the flow described in User Resolution and Checkout and APIs.

Response to the frontend

Return a success payload that includes whatever the frontend needs to open the payment UI (e.g. order id, amount, key, and other config). The frontend does not need to know whether the cart was anonymous; that is handled entirely on the backend when the payment is confirmed.

User Resolution: Registered vs Unregistered (Search User vs Create User)

This document describes how the backend resolves a user ID for anonymous cart checkout: when to call the search user API (registered user, not logged in) and when to call the create user API (unregistered user).

When user resolution runs

User resolution runs only when all of the following are true:

  1. The order is marked as anonymous (e.g. is_anonymous_cart === true).
  2. Platform context is available (e.g. company_id and application_id or equivalent).
  3. The flow has reached the step where the platform “checkout cart” API is about to be called (and that API requires a user_id).

It does not run when the user is logged in; in that case user_id is taken from the request or session.

Required input: phone number

For anonymous checkout, a phone number is required to either:

  • Search for an existing user (registered), or
  • Create a new user (unregistered).

If phone is missing, the flow should fail with an explicit error (e.g. “Phone number is required for anonymous checkout”) and must not call the platform checkout API without a valid user_id.

Where phone comes from

Phone is typically taken from the customer details provided at checkout or by the payment gateway (e.g. shipping address, billing address, or top-level contact field).

Use a single, well-defined precedence (e.g. contact first, then shipping, then billing) and document it.

Step 1: Search user (registered user path)

First, search for an existing user using the normalized phone.

Example

// Generic: call platform "search user" API (e.g. by phone)
const searchResponse = await platformClient.user.searchUsers({
q: normalizedPhone // or platform-specific query param
});

const users = searchResponse?.users ?? searchResponse?.items ?? [];

If a user is found (registered user, not logged in)

  • Take the first matching user (or apply your own rules).
  • Read user_id from the user object (field name may be user_id, _id, or id depending on the platform).
  • Set userId = user.user_id || user._id || user.id.
if (users.length > 0) {
const user = users[0];
userId = user.user_id ?? user._id ?? user.id;
// Proceed to checkout with userId (registered user, not logged in)
return userId;
}

Step 2: Create user (unregistered user path)

If the search returns no users, the customer is treated as unregistered. Create a new user with the same normalized phone and any available name/address data, then use the new user’s ID.

Example

// Generic: call platform "create user" API
const createResponse = await platformClient.user.createUser({
body: {
first_name: firstName,
last_name: lastName,
phone_number: normalizedPhone,
// ... other fields required by your platform (e.g. gender, verified: false)
},
// ... scope params e.g. companyId, applicationId if required
});

userId = createResponse?.user?._id ?? createResponse?.user?.id;

Name can be derived from shipping/billing name or a single “name” field (e.g. split on first space into first/last). If nothing is available, use a placeholder (e.g. "Customer") per your product requirements.

Handling Existing User

Some platforms may return an error when create is called for a phone number that already exists (e.g. “User exists”). In that case:

  1. Retry search with the same normalized phone.
  2. If search now returns a user, use that user_id (same as registered path).
  3. If search still returns no user, treat as an error and fail the flow with a clear message (e.g. “User exists with this phone but could not be found”).
try {
const createResponse = await platformClient.user.createUser({ ... });
userId = createResponse?.user?._id;
} catch (createError) {
if (isUserAlreadyExistsError(createError)) {
const retrySearch = await platformClient.user.searchUsers({ q: normalizedPhone });
const retryUsers = retrySearch?.users ?? retrySearch?.items ?? [];
if (retryUsers.length > 0) {
userId = retryUsers[0].user_id ?? retryUsers[0]._id ?? retryUsers[0].id;
} else {
throw new Error('User exists with this phone but could not be found.');
}
} else {
throw createError;
}
}

Platform Checkout API and Anonymous Cart Headers

This document describes how the backend calls the platform checkout (e.g. “checkout cart”) API and other cart-related APIs when handling an anonymous cart: which headers to send, how to pass user_id, and how to perform cart operations (get cart, remove coupon, delete cart) in the anonymous context.

Prerequisites

Before calling the platform “checkout cart” API you must have:

  1. user_id: From session (logged in) or from User Resolution (search or create) for anonymous carts.
  2. cart_id: From order meta (stored at order creation).
  3. Addresses and payment info: From the payment gateway callback or webhook (e.g. shipping/billing, payment method).

If the order is anonymous and user resolution fails (e.g. no phone), do not call checkout; return a clear error to the caller.

Anonymous cart header

Many platforms require a special header to indicate that the request is for an anonymous cart, so that:

  • The cart is identified by cart_id (or equivalent) and not by the authenticated user.
  • The platform may apply different rules (e.g. no user-based promotions, or linking the order to the resolved user_id).

A typical header name is:

x-anonymous-cart: true

Send this header on every request that touches the anonymous cart: get cart, checkout cart, remove coupon, delete cart, etc., when is_anonymous_cart is true.

Example

// Generic: build headers for platform API calls
const headers = {
'Content-Type': 'application/json',
// ... other required headers (e.g. ordering source)
};

if (order.meta?.is_anonymous_cart) {
headers['x-anonymous-cart'] = 'true';
}

Use the same headers (or requestHeaders) when calling the platform’s cart and checkout APIs for that order.

Adding user_id to the Checkout Payload

The platform “checkout cart” API usually expects a user_id in the body (or in context) so the created order is attached to the correct user. For anonymous checkout, this is the user_id obtained from the search user or create user step.

Example

// Generic: build checkout payload
const checkoutPayload = {
id: cartId,
delivery_address: shippingAddress,
billing_address: billingAddress,
payment_mode: paymentMode,
callback_url: callbackUrl,
// ... other required fields
};

if (userId) {
checkoutPayload.user_id = userId;
} else if (order.meta?.is_anonymous_cart) {
throw new Error('user_id is required for anonymous checkout but was not found or created');
}

So, for anonymous carts, user_id must be set (by user resolution) before building the payload; if it is still missing, fail with an explicit error.

Calling the Platform Checkout API

Call the platform’s “checkout cart” (or equivalent) API with:

  • The cart identifier (e.g. cart_id from order meta).
  • The checkout payload (addresses, payment mode, user_id, etc.).
  • The request headers including x-anonymous-cart: true when the order is anonymous.
// Generic: call platform checkout API
const checkoutResponse = await platformClient.cart.checkoutCart({
id: cartId,
body: checkoutPayload,
requestHeaders: headers,
});

Other Cart Operations with Anonymous Header

Any other cart API called in the same flow (before or after checkout) must also send the anonymous header when the order is anonymous, so the platform applies the correct context.

Get cart

When you need the current cart state (e.g. to compare totals or read applied coupon):

const getCartParams = {
id: cartId,
// ... other params (e.g. buyNow, includeBreakup)
};

if (order.meta?.is_anonymous_cart) {
getCartParams.requestHeaders = { 'x-anonymous-cart': 'true' };
}

const currentCart = await platformClient.cart.getCart(getCartParams);

Remove coupon

If you remove a coupon before checkout (e.g. when the payment gateway indicates no coupon):

const removeCouponParams = {
uid: cartId,
body: { coupon_code: couponCode },
// ...
};

if (order.meta?.is_anonymous_cart) {
removeCouponParams.requestHeaders = { 'x-anonymous-cart': 'true' };
}

await platformClient.cart.removeCoupon(removeCouponParams);

Delete cart (post-payment)

After a successful payment and platform order creation, you may want to delete the anonymous cart so it is not reused. Delete should use the same cart_id and anonymous header. See Post Payment.

await platformClient.cart.deleteCart({
id: cartId,
body: {},
requestHeaders: { 'x-anonymous-cart': 'true' },
});

Do not pass user context for anonymous cart operations

For anonymous carts, the platform identifies the cart by cart_id and the x-anonymous-cart header. Do not send a user token or session that would override this (e.g. do not attach the logged-in user’s auth to the request). The only user identifier that should be sent is the user_id inside the checkout payload (the one from search/create), not in the auth context of the cart APIs.

Post-Payment: Cart Clearing and Webhook vs Callback

This section describes what happens after payment succeeds for an anonymous cart: when and how to clear (delete) the anonymous cart, and how the flow differs when the backend is invoked via a payment gateway webhook versus a browser redirect/callback. The content is generic and can be applied to any payment provider and platform.

After a successful payment and successful platform “checkout cart” (order created on the platform). The cart has been “consumed” by the order.

So for anonymous checkout, after both payment and platform checkout succeed, the backend should call the platform’s delete cart API for that cart_id, with the anonymous cart header.

When to clear

Clear the cart only when all of the following are true:

  1. The order is anonymous: is_anonymous_cart === true.
  2. Payment has been confirmed (e.g. payment captured or COD order confirmed).
  3. Platform checkout (e.g. “checkout cart”) has completed successfully (order created on the platform).
  4. You have a valid cart_id in order meta (cart_id or equivalent).

If any of these is false, skip cart deletion (e.g. logged-in users might have different cart lifecycle rules).

How to clear: delete cart API

Use the same cart_id and x-anonymous-cart header used for checkout:

// Generic: clear anonymous cart after successful payment + checkout
if (order.meta?.is_anonymous_cart && cartId) {
try {
await platformClient.cart.deleteCart({
id: cartId,
body: {},
requestHeaders: { 'x-anonymous-cart': 'true' },
});
} catch (err) {
// Log but do not fail the payment/order flow
logger.error('Failed to clear anonymous cart after payment', {
cart_id: cartId,
order_id: order.id,
error: err.message,
});
}
}

Where to run cart clearing

Cart clearing can happen in two places depending on who calls your backend after payment:

  1. Payment verification handler (browser callback): The user is redirected to your success URL with payment/order identifiers; your backend verifies the payment with the gateway, then runs platform checkout (if not already done) and then clears the anonymous cart if applicable.

  2. Webhook handler (payment gateway → your server): The payment gateway sends an HTTP request to your webhook URL when payment is captured (or COD confirmed). Your backend runs the same flow: ensure platform checkout ran (and run it if needed), then clear the anonymous cart.