Uber Eats Restaurant Items
v1PublishedThe full menu catalog (categories and items with prices, calories, and details) for any Uber Eats store page.
›Author's sample data
| items | |
|---|---|
| store | |
| itemCount | 206 |
| categories | |
| categoryCount | 17 |
›Publisher
2 subscribersEvery hour6 runs in 14d · published 22h ago
›Parameters
--urlstringThe Uber Eats store page URL to scrape the menu from. default "https://www.ubereats.com/store/starbucks-1750-divisadero-street/frb7Q3ZbQOKg15qMgdBjkA" · e.g. "https://www.ubereats.com/store/starbucks-1750-divisadero-street/frb7Q3ZbQOKg15qMgdBjkA"
›Versions
managed by authorv1builtapprovedcurrent22h ago
Schedulesdeploy to enable
Run this collector on a cadence — daily, hourly, your call.
API endpointdeploy to unlock
POST to run it on demand and get fresh data in the response.
Activitydeploy to track
6 subscriber runs in the last 14 days.
How this script collects data
import Firecrawl from "@mendable/firecrawl-js";
import * as cheerio from "cheerio";
import { parseArgs } from "node:util";
const apiKey = process.env.FIRECRAWL_API_KEY;
if (!apiKey) {
console.error("FIRECRAWL_API_KEY is not set");
process.exit(1);
}
const firecrawl = new Firecrawl({ apiKey });
// --url lets the script target any Uber Eats store page; defaults to the requested Starbucks store.
const { values: flags } = parseArgs({
strict: true,
options: {
url: { type: "string" },
},
});
const storeUrl =
flags.url ??
"https://www.ubereats.com/store/starbucks-1750-divisadero-street/frb7Q3ZbQOKg15qMgdBjkA";
/**
* Uber Eats embeds the full store catalog as JSON inside a
* <script id="__REACT_QUERY_STATE__"> tag. That JSON is HTML-safe-encoded:
* every backslash in the original JSON is written as the literal text "%5C",
* and the structural quotes/brackets are written as \uXXXX escapes. We reverse
* both transforms to recover the real JSON, then read the getStoreV1 payload.
*/
function decodeReactQueryState(scriptText: string): any {
const unescaped = JSON.parse('"' + scriptText.trim() + '"'); // " -> " etc.
const restored = unescaped.replace(/%5C/g, "\\"); // %5C -> backslash
return JSON.parse(restored);
}
function dollars(cents: unknown): number | null {
return typeof cents === "number" ? Math.round(cents) / 100 : null;
}
// priceTagline reads like "$7.35 • 250 Cal." — pull the calorie figure out of it.
function caloriesFrom(tagline: unknown): number | null {
if (typeof tagline !== "string") return null;
const m = tagline.match(/([\d,]+)\s*Cal\./i);
return m ? Number(m[1].replace(/,/g, "")) : null;
}
async function main() {
console.error(`Scraping ${storeUrl}`);
const res = await firecrawl.scrape(storeUrl, { formats: ["rawHtml"] });
const html = (res as any).rawHtml ?? (res as any).html;
if (!html) throw new Error("No HTML returned from Firecrawl scrape");
const $ = cheerio.load(html);
const scriptText = $("script#__REACT_QUERY_STATE__").first().text();
if (!scriptText) throw new Error("Could not find __REACT_QUERY_STATE__ on the page");
const state = decodeReactQueryState(scriptText);
const queries: any[] = state.queries ?? [];
const storeQuery = queries.find(
(q) => Array.isArray(q.queryKey) && q.queryKey[0] === "getStoreV1",
);
if (!storeQuery) throw new Error("getStoreV1 query not found in embedded state");
const store = storeQuery.state.data;
const categories: Array<{ name: string; itemCount: number; items: any[] }> = [];
const allItems: any[] = [];
const seen = new Set<string>();
const catalogSectionsMap = store.catalogSectionsMap ?? {};
for (const sectionKey of Object.keys(catalogSectionsMap)) {
for (const entry of catalogSectionsMap[sectionKey]) {
// VERTICAL_GRID entries are the real menu categories. The HORIZONTAL_GRID
// "Featured items" carousel only re-lists items from those categories, so
// we skip it to keep the catalog free of duplicates.
if (entry.type !== "VERTICAL_GRID") continue;
const payload = entry.payload?.standardItemsPayload;
if (!payload) continue;
const categoryName: string = payload.title?.text ?? "Uncategorized";
const catalogItems: any[] = payload.catalogItems ?? [];
const items = catalogItems.map((it) => ({
uuid: it.uuid as string,
name: it.title as string,
description: (it.itemDescription as string) ?? null,
category: categoryName,
price: dollars(it.price),
priceText: it.priceTagline?.text ?? null,
calories: caloriesFrom(it.priceTagline?.text),
currencyCode: store.currencyCode ?? "USD",
imageUrl: (it.imageUrl as string) ?? null,
isSoldOut: Boolean(it.isSoldOut),
customizable: Boolean(it.hasCustomizations),
}));
categories.push({ name: categoryName, itemCount: items.length, items });
for (const item of items) {
if (seen.has(item.uuid)) continue;
seen.add(item.uuid);
allItems.push(item);
}
}
}
console.error(
`Found ${allItems.length} unique items across ${categories.length} categories`,
);
const out = {
store: {
name: store.title ?? null,
uuid: store.uuid ?? null,
slug: store.slug ?? null,
city: store.citySlug ?? null,
currencyCode: store.currencyCode ?? "USD",
rating: store.rating?.ratingValue ?? null,
url: storeUrl,
},
categoryCount: categories.length,
itemCount: allItems.length,
categories: categories.map((c) => ({ name: c.name, itemCount: c.itemCount })),
items: allItems,
};
process.stdout.write(JSON.stringify(out));
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Build prompt
Uber Eats Restaurant Items