Keywords: abandoned cart recovery, cart abandonment, win-back, ecommerce revenue, checkout recovery, abandoned checkout, cart recovery email, recovery incentive, Shopify abandoned checkout, WooCommerce cart
Give Cartback an abandoned cart (items, value, customer, time since abandonment). It returns a staged recovery plan and a drafted nudge, with an incentive only when the expected value justifies it. It does not send the message, and it does not apply a discount.
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
cartback demoNo API key. The planner uses the rule-based policy and templates. You get a stage, a channel, an incentive decision, and a drafted nudge for each sample cart.
Same path on the example file:
cartback plan examples/cart.json
python examples/recover.pyTo draft with an LLM instead, set one model name and the provider key. LiteLLM reads the key from the provider's usual env var.
| Provider | LLM_MODEL |
Key |
|---|---|---|
| OpenAI | gpt-4o |
OPENAI_API_KEY |
| Anthropic | claude-opus-5 |
ANTHROPIC_API_KEY |
gemini/gemini-2.0-flash |
GEMINI_API_KEY |
|
| OpenRouter | openrouter/openai/gpt-4o |
OPENROUTER_API_KEY |
| Ollama (local) | ollama/llama3.2 |
none |
| vLLM (local) | hosted_vllm/Qwen/Qwen2.5-7B-Instruct |
none |
Default model when a key is set and LLM_MODEL is unset: gpt-4o-mini.
# OpenAI
export LLM_MODEL=gpt-4o
export OPENAI_API_KEY=sk-your-key
cartback plan examples/cart.json
# Anthropic
export LLM_MODEL=claude-opus-5
export ANTHROPIC_API_KEY=sk-ant-your-key
cartback plan examples/cart.json
# Local Ollama (run `ollama serve` and pull the model first)
export LLM_MODEL=ollama/llama3.2
cartback plan examples/cart.json--no-agent forces the templates even when a key is present.
Cartback is a small Python planner for abandoned cart recovery and checkout win-back. Stores already know that a cart sat unpaid. Cartback decides what to do next: when to nudge, on which channel, and whether a discount or free shipping is worth the margin. It also drafts the email, SMS, or WhatsApp copy for that step.
v0 is decide-and-draft only. Your own code sends the message and applies a coupon when you are ready.
| Piece | Role |
|---|---|
PolicyEngine |
Pure stage clock and incentive rule. No API key. This is the demoable core. |
RecoveryAgent |
Optional model loop (any provider via LiteLLM, LLM_MODEL) for tone and the strongest reason to come back. Falls back to templates with no key. |
| Simulated generator | Builds a fixed mix of abandoned carts for cartback demo. No live store. |
| Shopify / WooCommerce stubs | Map public JSON field shapes onto Cart. No keys. No network. |
| CLI | cartback plan and cartback demo print the plan and the draft. |
How do I recover an abandoned cart?
Load the cart as a Cart (items, customer, abandoned_at) and call PolicyEngine().plan(cart). At 1 hour the default plan is an early reminder with no discount. At 24 hours it is a value reminder, and a small percent off only if cart value and margin clear the incentive rule. At 72 hours it is a last-chance nudge. After 168 hours, or after every stage has already been sent, recovery closes. Run cartback demo to see this on generated carts.
Does Cartback send the email or apply a coupon?
No. v0 never sends email, SMS, or WhatsApp, and never writes a discount to a store. Nudge.sent and Incentive.applied stay False. Take the RecoveryPlan and Nudge and send them from your own system.
Can I use this with Shopify or WooCommerce?
Yes, as data. Map an abandoned checkout or pending order with load_cart(..., fmt="shopify") or fmt="woocommerce". Field names follow the public docs (Shopify abandoned checkout, WooCommerce REST order / cart-contents). The loaders do not use API keys and do not open a network socket.
Lifecycle suites already email abandoned checkouts. Stores already have a "you left items" template. Cartback sits in the gap: a file you can read, a policy you can unit-test without a key, and a draft you can edit before anything reaches a customer. Payment agent studios sell this job as a product. This library is the open, testable core of that job.
| Store native emails | Lifecycle suite (Klaviyo and similar) | Homegrown cron | Cartback | |
|---|---|---|---|---|
| Works across stores | One store | That suite's connectors | If you write it | Yes, through loaders |
| Stage timing you can test offline | No | Partial | Yes | Yes (policy.py, no key) |
| Incentive only when margin allows it | You write it | Rules in the suite | You write it | Yes (decide_incentive) |
| Win-back copy (early / value / last chance) | One template | Templates in the suite | You write it | Drafted here, not sent |
| Sends messages in v0 | Store does | Suite does | Your cron does | Never |
| Needs an API key to demo | Yes | Yes | No | No |
Tradeoffs: Cartback will not send a message, will not create a coupon, and will not pull a live abandoned-cart list. The Shopify and WooCommerce classes are shape mappers from public JSON. If you already like your suite's send path, keep it and use this as the planner.
Everything below is what the code does, not a wish list.
- Input. A
Cartis items, aCustomer,abandoned_at, and optional recovery history. Amounts are minor units (paise, cents). - Stage. Hours since abandonment pick a stage from
(early_reminder, value_reminder, last_chance)at delays(1.0, 24.0, 72.0)hours. If the cart is first seen late, the planner skips stale copy and uses the latest due stage.PolicyConfigcan replace both tuples andexpire_hours(default 168). - Channel.
preferred_channelwins when the matching address exists (email,sms,whatsapp). Otherwise email, then SMS if only a phone is present. - Incentive.
decide_incentivestays at list price when any of these hold: the stage isearly_reminder; cart value is belowmin_cart_value; blended margin is belowmin_margin_bps; a coupon is already on the cart; the shopper hasorder_count >= loyal_order_count; a percent off would leave too little margin; expected recovery value with the discount is not higher than without it. Otherwise it draftsfree_shipping(when shipping is a small share of the cart) orpercent_off(5% at value reminder, 10% at last chance). The code is a draft. Nothing is applied. - Reason.
strongest_reasonprefers low stock, then a near free-shipping threshold, then the highest-value line item. - Copy.
draft_nudgefills a template for the stage and channel. Closed carts become an internal note addressed toops. - Optional model.
RecoveryAgentalways runs the policy first. If a provider key is set (orLLM_MODELis a local Ollama / vLLM string), it starts a tool loop (get_cart,get_customer,run_policy,submit_nudge) through LiteLLM. Arefusalstop reason, a missing SDK, a missing key,--no-agent, or any exception falls back to the templates. The model is for judgment and tone, not for sending. - Output. CLI prints the plan and the draft. Nothing is sent.
abandoned cart -> stage + delay -> incentive rule
| |
v v
channel + reason none / free ship / % off
| |
v v
RecoveryPlan + drafted Nudge
|
v
your integration sends (not v0)
Python 3.11 or newer.
git clone https://github.com/pandeyvishwas51-oss/cartback.git
cd cartback
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest -qRuntime dependency: litellm (called only if a provider key is set, or if LLM_MODEL points at a local Ollama / vLLM endpoint). The policy engine, templates, simulated carts, and CLI --no-agent path do not call the network. Pydantic is optional: pip install "cartback[pydantic]".
cartback demo
cartback plan examples/cart.json
cartback --json --no-agent plan examples/cart.jsoncartback demo covers a fresh cart still waiting, an early reminder, a value reminder with a justified percent off, a last-chance nudge, a cart too small to discount, a thin-margin cart, a repeat buyer, an already-discounted cart, low stock, near free shipping, WhatsApp, an exhausted sequence, and an expired cart.
from datetime import datetime, timezone
from cartback import PolicyEngine, Cart, CartItem, Customer
now = datetime(2026, 8, 21, 12, 0, tzinfo=timezone.utc)
cart = Cart(
id="cart_ada",
items=[CartItem(sku="NB-1", name="Notebook", quantity=1, unit_price=79900, margin_bps=4200)],
currency="INR",
abandoned_at=datetime(2026, 8, 21, 10, 0, tzinfo=timezone.utc),
customer=Customer(
id="cus_ada",
name="Ada Lovelace",
email="ada@example.com",
),
checkout_url="https://example.com/c/ada",
)
plan = PolicyEngine().plan(cart, now=now)
print(plan.current_stage, plan.channel, plan.incentive.kind, plan.rationale)
# early_reminder email none early_reminder for cart cart_ada (79900 INR) abandoned 2.0h ago; channel email; incentive nonefrom cartback import RecoveryAgent
result = RecoveryAgent(force_templates=True).recover(cart, now=now)
print(result.nudge.subject)
print(result.nudge.body)
print(result.nudge.sent) # always FalseSet LLM_MODEL and the provider key if you want the model loop to draft instead of the templates. Use --no-agent on the CLI to force templates even when a key is present.
from cartback.sources import load_cart
from cartback import PolicyEngine
cart = load_cart("examples/shopify_checkout.json", fmt="shopify")
plan = PolicyEngine().plan(cart)
cart = load_cart("examples/woocommerce_cart.json", fmt="woocommerce")
plan = PolicyEngine().plan(cart)The loaders do not read store credentials. There is no live list of abandoned checkouts in v0.
from cartback import PolicyConfig, PolicyEngine
config = PolicyConfig(
stage_delays_hours=(0.5, 12.0, 48.0),
expire_hours=96.0,
min_cart_value=20_000,
min_margin_bps=3000,
)
plan = PolicyEngine(config).plan(cart)- Run Cartback on each abandoned cart.
- If
next_step.kind == "nudge", sendNudgeyourself atscheduled_at. - If
incentive.kindis notnone, create the coupon in your own promo system, then send. Do not assume Cartback applied it. - If
kind == "close", stop. Do not keep emailing. - Append a
RecoveryAttemptto history and re-run the planner later.
Python 3.11+. Tests run on 3.11, 3.12, and 3.13 in CI.
No. The policy engine, templates, and simulated demo run without one. A key is only for the optional model loop. LiteLLM reads OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY, and the other standard provider env vars. Local Ollama needs LLM_MODEL=ollama/<name> and no key.
LLM_MODEL if set, otherwise gpt-4o-mini (cartback.DEFAULT_MODEL). A refusal stop reason is treated as a fallback to templates, not as a crash.
Early reminders are for memory, not for training shoppers to wait. The incentive rule starts at value_reminder.
Thin blended margin, an existing coupon, a repeat buyer (order_count), or an expected-value check that says the lift does not cover the giveaway. Read plan.incentive.rationale.
Write a mapper that returns a Cart from that store's public JSON, the way sources/shopify.py and sources/woocommerce.py do. Keep sends and coupon writes out of that mapping.
Not in v0. The plan may include a draft code such as SAVE5 or FREESHIP. You create it in the store if you want it live.
Yes. CI unsets provider keys, LLM_MODEL, and store tokens. The LiteLLM layer is mocked or skipped. Shopify and WooCommerce tests cover payload mapping only.
pip install -e ".[dev]"
pytest -qMIT. Free for commercial and personal use.