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.
Related
- Orders & fulfillment — order creation and fulfillment are separate from cart capture