Abandoned cart recovery
Your storefront reports in-progress carts; litecommerce stores them, and a
recovery cron emails customers who left one behind. 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. A cart with no email
can be stored but can't be emailed a recovery. 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.
How recovery works
The recovery send is live and automated:
- A scheduled cron sweeps carts that have an email and aren't yet recovered
(
recoveredAt IS NULL). - It sends a recovery email via Resend and stamps
recoveryEmailSentAtandrecoveryEmailCount. The send is capped (an unconverted cart gets at most a couple of nudges spaced out over ~a day, never an indefinite stream).
You don't trigger any of this — capturing the cart (with an email) is all your storefront does; the platform handles the send cadence.
Conversion attribution isn't wired yet.
recoveredAtexists as a column and therecovered/allmerchant filters read it, but nothing stamps it when a captured cart turns into an order today — so in practice every captured cart stays in thependinglist, and the send cap (not a conversion signal) is what stops the emails. Closing that loop — matching a new order back to its cart and stampingrecoveredAt— is a planned follow-up that pairs naturally with order placement at hosted checkout.
Related
- Orders & fulfillment — what a recovered cart becomes
- Pricing rules — a recovery email can feature a coupon