The shopping list is the most-used piece of software in my house, and for years it was three shopping lists: whatever was on the Echo, whatever was in my wife’s notes app, and whatever I’d told myself I’d remember.
The reason is simple and nobody has solved it. Items get added by voice — you’re standing at the fridge with a carton in your hand and wet hands, you say “add milk,” and that’s the entire interaction. But items get used on a phone, in a store, sorted by aisle, checked off one-handed. The capture surface and the consumption surface are different devices, and the good list apps only own one of them.
So the list lives in the Echo, where it’s useless in a grocery store, and nobody can see it but the person who said it.
The API Amazon killed
The obvious fix is to sync Alexa’s list somewhere better. Amazon closed that door on July 1, 2024, when they shut off the official Lists API. That’s the exact date third-party sync died across the board — it’s why AnyList and Todoist integrations stopped working, all on the same day, and never came back.
What survived is unofficial: the Alexa app itself still talks to Amazon over an internal API, and the Home Assistant community maintains an integration that speaks it directly. I want to be precise about credit here, because it matters — I didn’t reverse-engineer Amazon’s endpoints. lonlazer/ha-alexa-todo-lists did that work. I found it, wired it into a real system, and then built everything above it.
What I did have to accept is what that foundation costs:
- It requires an authenticator-app OTP to link, which meant turning on app-based 2FA for the household Amazon account specifically to hold this thing together.
- The push direction is instant. The pull direction is a 10-minute poll, because there is no realtime path anymore.
- It is the same class of dependency as any unofficial API: Amazon can break it without notice, and the symptom will be silent — lists just stop updating.
That 10-minute poll, plus my own 2-minute reconcile loop, means a voice add can take up to twelve minutes to appear in the app. Going the other way takes seconds. That asymmetry isn’t a bug I failed to fix; it’s the shape of what’s possible now, and the app is designed around it rather than pretending otherwise.
There is no scratch space
Here’s the rule that I had to learn once and never forget, and it’s written in capitals in my notes:
shop IS Alexa. Anything added in the app — typed, emailed, from a recipe, from a text message — pushes to Home Assistant and relays to Amazon within seconds. It appears on the actual Echo in the actual kitchen.
Which means there is no such thing as a test row. Early on I QA’d against the live instance and put junk on my family’s real shopping list, where it also round-tripped back every two minutes so deleting it locally accomplished nothing. Cleanup is two-sided and order-dependent: remove it upstream first, then locally, or it comes right back.
Every subsequent round of work has been developed against a throwaway database with the sync sources emptied out. The production list is a shared family surface, and the blast radius of a bug is “Mom sees __probe2__ at the grocery store.”
Merge semantics: last-write-wins, but done always wins
Two-way sync between systems that can’t see each other needs a conflict rule. Mine is last-write-wins per item, with one override: a check-off is never resurrected by a stale sync.
That single exception covers the realistic failure. You’re in the store, you check off milk, and forty seconds later a poll arrives carrying a snapshot from before you did. Plain LWW puts milk back on your list while you’re standing in the dairy aisle. Done-wins means the check-off is sticky, and the worst case is an item briefly reappearing that you can check off again — instead of the app confidently un-buying your groceries.
Items removed on the Alexa side become done rather than deleted, so the history survives. That decision turned out to matter more than anything else in the app.
The history is the actual product
Once you keep every purchase with a timestamp, the list stops being a list and starts being a model of how a household runs.
Staples. For every item, the app computes the median interval between purchases from its own done-history and flags what’s probably out. The UI says it in plain language: bought 6 days ago · usually every 5 days. There’s no AI in this at all — it’s a SQL query over your own behavior, and it’s the feature the family actually notices.
It also takes feedback, which is where it gets opinionated. Accepting a suggestion is a signal. Dismissing one snoozes it, scaled by how many times you’ve dismissed it. Three dismissals mute the item entirely — until you actually buy it again, at which point the feedback resets and it re-enters the rotation. The app is allowed to be wrong about you, but it has to stop repeating itself.
Aisle order that learns your store. A nightly job watches the order you check things off while you’re physically walking the store, and re-sorts the categories to match. No settings, no UI, nobody configures anything. And because the two houses shop at different stores, each house learns its own walk independently — one supermarket’s layout never contaminates the other’s.
Ask-once AI. New item names get categorized by a keyword map first, which is free and handles most of them. Only a miss escalates to a small fast model — and the answer is written back into the keyword table, so any given item name costs one API call in the entire lifetime of the app. You can watch an item hop to its correct aisle a second after you add it. The spend trends toward zero as the vocabulary fills in, which is my favorite property of any AI feature I’ve built: it makes itself obsolete.
The same ask-once pattern got reused for canonicalization — recognizing that “oj” and “orange juice” and “stick butter” and “butter” are the same product for history purposes. Grouping only; the display name you typed is never rewritten, because the original string is what matches back to Alexa.
The best trick in the app
We eat the same nine dinners. So the app knows them.
Say “Alexa, add tacos” and it doesn’t add the word tacos. It compiles the meal — adds the ingredients, skips what’s already on the list, and skips what you probably still have (conservatively: it needs at least three purchase events and a recent-enough last purchase before it assumes your pantry).
The mechanics of making that work through a two-way sync are the fun part. The trigger item gets inserted and then immediately marked done, so the done-wins rule clears the word “tacos” off the Echo without a fight. I proved the no-loop invariant with an end-to-end test against a mock: second reconcile, zero duplicates, zero re-explosions.
The gotcha this creates makes me laugh: the nine meal names are reserved words. Name a meal something you’d ever want to literally buy, and you can never put it on the list again.
Two houses
The list is now per-household — one for Muskoka, one for Florida — and what’s shared versus isolated took real thought.
Per house: the list itself, purchase history, staples cadence, aisle order, the dinner rotation, the Echo binding, notifications. Adding taco shells in Florida never pings anyone in Ontario.
Shared: the vocabulary. “sc” still means sour cream in both houses, and category names stay consistent. Language is a family-level fact; pantry rhythm is a house-level fact.
That migration also produced my favorite recent bug. A database index referencing a new column got created in the shared pre-migration block, before the guard that adds the column — so it crashed on every boot against the real database while 228 tests passed cleanly against fresh ones. Two minutes of downtime and a permanent lesson: migration tests need a case that starts from an existing database shape, not just an empty one and an idempotence check.
The list also feeds the morning brief, which pulls it live at send time so the “running low” predictions come with one-tap add links in the email.
What happens when Amazon turns it off
They already did once. The current path is unofficial, undocumented, and entirely at their discretion — one internal change and the voice leg dies without an error message.
I built on it anyway, and I’d do it again, because the failure is survivable by design. If the bridge goes, the app still works: phones, email ingestion, recipes, meals, staples, history — all of it lives in my database, on my server. What I’d lose is the ability to say “add milk” with wet hands.
Which, admittedly, is the whole reason I started.