Skip to content

Abandoned cart capture and review

Your storefront can report in-progress carts for merchant review in admin. Recovery emails are unavailable. Capturing a cart or an email address does not enable outbound recovery. Examples reuse the api() helper.

Capture a cart (public)

POST /api/v1/public/abandoned-carts — tenant header, no auth. Call it as the cart changes; it upserts on sessionId, so repeated calls update one row rather than piling up duplicates.

Because the endpoint is anonymous and the sessionId isn't a secret, updating a cart requires a capture token: a high-entropy value your storefront generates once, stores next to the sessionId, and sends on every call. The first capture binds it; later updates must present the same token or are rejected with 403. Generate it client-side and reuse it — the API never returns it. (A capture sent without a token still works, but creates an unprotected row anyone who learns the sessionId could overwrite, so always send one.)

// Generate + persist both once per browser, then reuse on every capture.
function captureIds() {
  let sessionId = localStorage.getItem("lc_cart_session");
  if (!sessionId) {
    sessionId = crypto.randomUUID();
    localStorage.setItem("lc_cart_session", sessionId);
  }
  let captureToken = localStorage.getItem("lc_cart_token");
  if (!captureToken) {
    const bytes = crypto.getRandomValues(new Uint8Array(32));
    captureToken = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
    localStorage.setItem("lc_cart_token", captureToken);
  }
  return { sessionId, captureToken };
}
 
const { sessionId, captureToken } = captureIds();
 
await api("/public/abandoned-carts", {
  method: "POST",
  body: JSON.stringify({
    sessionId,                    // required, 8–200 — your storefront session id
    captureToken,                 // your generated token, 16–128 — send it every time
    snapshot: {                   // required — frozen cart state
      items: [{ itemId: "…", variantId: "…", name: "Trail Pack 38L", quantity: 1, unitPriceInCents: 18900 }],
      subtotalInCents: 18900,
      totalInCents: 18900,
    },
    email: "dana@example.com",    // optional — capture once you have it
  }),
});
// → { ok: true, wasCreated: boolean }   (no token returned — you already hold it)

email is optional so you can capture the cart before you know who it belongs to, then fill it in once the shopper enters it. Storing an address is not consent or authorization to send recovery email. Once an email is set, it's frozen — a later capture won't overwrite it with a different address.

Review captures (merchant)

GET /api/v1/merchant/abandoned-carts?filter=pending — open to any member. filter is pending (not yet recovered, the default), recovered, or all.

Outbound recovery is unavailable

Capture, merchant review, template editing, and scheduling foundations exist. They do not make recovery delivery available: ABANDONED_CART_RECOVERY is a marketing flow denied on the shared transactional transport. The outbox stops it before reading the cart or calling the provider. A template preview or a successful Send test uses a separate test flow and does not prove recovery eligibility.

Outbound recovery remains a separate product outcome. There is no scheduled rollout to build against. An accepted recovery charter must address recipient and consent capture, conversion attribution, preferences and suppression, and an isolated marketing transport before delivery can be offered. The governing boundaries are #3273, #3350, and #3351.

Interpreting recorded state

recoveredAt and the recovered / all filters remain part of the stored contract, but order creation does not currently stamp a captured cart as recovered. Likewise, recoveryEmailCount and recoveryEmailSentAt are recorded fields, not evidence that a recovery automation is enabled. Do not infer a send or conversion from capture success.