You are working with the Menu Intelligence API, a REST API that serves structured restaurant menu data for 180+ NYC restaurants with 14,000+ dishes. The data is scraped monthly and includes prices, ingredients, allergens, trends, and geographic data.
https://api.cartewei.dev
CORS is enabled (Access-Control-Allow-Origin: *), so you can fetch directly from browser-based apps.
Your API key will be provided by the Cartewei team. It starts with cw_ and is used via the X-API-Key header on all /api/v1/ requests.
Before making data requests, you must accept the current terms:
# Check current terms
curl https://api.cartewei.dev/api/v1/terms/current
# Accept terms
curl -X POST https://api.cartewei.dev/api/v1/terms/accept \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"version": "2026-02-14-v1"}'
# List all restaurants
curl https://api.cartewei.dev/api/v1/restaurants \
-H "X-API-Key: YOUR_API_KEY"
# Get allergen profile for a restaurant
curl https://api.cartewei.dev/api/v1/restaurants/RESTAURANT_ID/allergen-profile \
-H "X-API-Key: YOUR_API_KEY"
# Get allergens for a specific dish
curl https://api.cartewei.dev/api/v1/dishes/DISH_ID/allergens \
-H "X-API-Key: YOUR_API_KEY"
| Status | Error | Meaning |
|---|---|---|
| 401 | api_key_required |
Missing X-API-Key header |
| 401 | invalid_api_key |
Key not found or revoked |
| 403 | terms_not_accepted |
Call /api/v1/terms/accept first |
| 403 | terms_update_required |
New terms version — re-accept |
| 429 | Too many requests | Rate limit: 100 requests/minute |
Every response includes a meta block with legal and versioning info:
{
"restaurants": [ ... ],
"meta": {
"api_version": "v1",
"terms_version": "2026-02-14-v1",
"terms_accepted_at": "2026-03-13T12:00:00Z",
"data_classification": "informational_only",
"legal_notice": "NOT_SAFETY_GUIDANCE"
}
}
GET /api/restaurants
Returns all restaurants with metadata.
{
"restaurants": [
{
"id": "cmjxsfsfe02iz1knukuldkafc",
"name": "Al Badawi",
"url": "https://...",
"neighborhood": "Yorkville",
"cuisineType": "Middle Eastern",
"menuCount": 1,
"hasPrixFixe": false,
"latestMenuDate": "2026-01-03T04:13:44.799Z"
}
]
}
GET /api/restaurants/:id/menus
GET /api/restaurants/:id/menus/:type # type: "food" or "beverage"
GET /api/restaurants/:id/menus/:type/history # historical menu versions
GET /api/restaurants/:id/snapshots # raw snapshot list
GET /api/restaurants/:id/scraping-rounds # scraping metadata
GET /api/restaurants/:id/diff # menu changes between snapshots
GET /api/dishes/popular
Cross-restaurant dish comparison with price ranges.
{
"dishes": [
{ "canonicalName": "Tiramisu", "restaurantCount": 15, "avgPrice": 13.87, "priceRange": "$8-$28.95" },
{ "canonicalName": "Chicken Parmigiana", "restaurantCount": 12, "avgPrice": 24.59, "priceRange": "$4.99-$38.95" }
]
}
GET /api/dishes/compare?dish=margherita+pizza # compare a specific dish across restaurants
GET /api/dishes/by-ingredient?ingredient=truffle # find dishes using a specific ingredient
GET /api/dishes/:id/allergens
Confidence-scored allergen data for a single dish (0.00–0.99 scale, never claims certainty).
{
"dish_id": "...",
"dish_name": "Caesar Salad",
"allergens": [
{
"allergen": "dairy",
"fda_category": "milk",
"confidence": 0.70,
"verification_tier": "high_confidence",
"basis": "ingredient_match",
"source_ingredient": "parmesan"
}
],
"allergen_summary": {
"detected_allergens": ["dairy", "gluten"],
"min_confidence": 0.45,
"max_confidence": 0.70,
"overall_verification_tier": "moderate",
"recommendation": "INFORMATIONAL_ONLY"
}
}
GET /api/restaurants/:id/allergen-profile # all dishes' allergens for a restaurant
Important: Allergen data is informational_only and NOT_SAFETY_GUIDANCE. All API responses include a meta block with this classification.
GET /api/ingredients # list canonical ingredients
GET /api/ingredients/search?q=truffle # search ingredients
GET /api/ingredients/categories # ingredient categories (protein, vegetable, etc.)
GET /api/ingredients/dietary # dietary flags (vegan, gluten-free, etc.)
GET /api/ingredients/top # most common ingredients
Top ingredients response:
{
"ingredients": [
{ "ingredient": "garlic", "dishCount": 367, "restaurantCount": 65, "avgPrice": 22.73, "priceRange": { "min": 7.35, "max": 99 } },
{ "ingredient": "tomato", "dishCount": 213, "restaurantCount": 48, "avgPrice": 24.69 }
]
}
GET /api/ingredients/heatmap # ingredient frequency heatmap data
GET /api/ingredients/trends/:ingredient # usage over time for one ingredient
GET /api/ingredients/locations # ingredients grouped by neighborhood
GET /api/ingredients/by-location/:location # ingredients for a specific area
GET /api/beverages # all beverages
GET /api/beverages/compare # cross-restaurant beverage comparison
GET /api/beverages/by-restaurant/:id # beverages for a restaurant
GET /api/restaurants/:id/beverage-stats # beverage analytics for a restaurant
GET /api/analytics/diversity # ingredient diversity per restaurant
GET /api/analytics/stability # menu stability index (how often menus change)
GET /api/analytics/by-borough # aggregate stats by NYC borough
GET /api/analytics/by-city # aggregate stats by city
GET /api/trends/overview
{
"overview": {
"totalRestaurants": 181,
"totalDishes": 14375,
"totalSnapshots": 409,
"totalChanges": 3228,
"cuisineBreakdown": [
{ "cuisine": "Italian", "count": 53 },
{ "cuisine": "American", "count": 11 }
]
}
}
GET /api/trends/prices # price trends over time
GET /api/trends/changes # menu change frequency over time
GET /api/trends/dishes # dish appearance/disappearance trends
GET /api/trends/stability # stability metrics over time
GET /api/trends/cuisines # cuisine representation trends
GET /api/map/restaurants # restaurants with lat/lng coordinates
GET /api/map/restaurants/:id/dishes # dishes for a specific restaurant (map context)
GET /api/map/filters # available filter options
GET /api/map/export/kml # KML export for Google Earth
Filters response includes:
{
"boroughs": ["Brooklyn", "Manhattan", "Queens", "The Bronx"],
"neighborhoods": ["Alphabet City", "Carroll Gardens", "Chelsea", ...],
"cuisineTypes": ["Italian", "American", "Mexican", ...]
}
GET /api/data-quality/issues # detected data quality issues
GET /api/data-quality/summary # issue counts by type/severity
GET /api/data-quality/restaurant/:id # issues for a specific restaurant
GET /api/time-machine/restaurants # restaurants with multiple snapshots for comparison
// Fetch all restaurants
const res = await fetch('http://localhost:3000/api/restaurants');
const { restaurants } = await res.json();
// Get popular dishes for a bar chart
const pop = await fetch('http://localhost:3000/api/dishes/popular');
const { dishes } = await pop.json();
// Get price trends for a line chart
const trends = await fetch('http://localhost:3000/api/trends/prices');
const priceData = await trends.json();
// Get geo data for a map
const geo = await fetch('http://localhost:3000/api/map/restaurants');
const { restaurants: geoRestaurants } = await geo.json();
// Get ingredient heatmap data
const heatmap = await fetch('http://localhost:3000/api/ingredients/heatmap');
const heatmapData = await heatmap.json();
// Get allergen profile for a restaurant
const allergens = await fetch(`http://localhost:3000/api/restaurants/${id}/allergen-profile`);
const allergenData = await allergens.json();
The dataset is well-suited for:
Menu PDFs are stored in private Supabase Storage (bucket: menu-pdfs). Access them through the API using signed URLs.
GET /api/restaurants/:id/pdf-url
Header: x-pdf-password: <password>
{
"url": "https://uhzewmdarxznwxecmexw.supabase.co/storage/v1/object/sign/menu-pdfs/...",
"restaurantName": "Via Carota",
"pdfInfo": {
"pageCount": 2,
"sizeBytes": 154320,
"capturedAt": "2026-01-15T00:00:00.000Z"
}
}
Important: Signed URLs expire after 1 hour. Download and cache the file itself, not the URL.
GET /api/restaurants/:id/pdf-history
Header: x-pdf-password: <password>
Returns signed URLs for all archived PDF versions.
GET /api/snapshots/:id/pdf-url
Returns signed URL for a specific snapshot's PDF (no password required).
Live screenshots of restaurant websites, generated via Browserless and cached as PNGs:
GET /api/canvas/thumbnail?url=https://viacarota.com
Content-Type: image/png)Cache-Control: public, max-age=86400)Access-Control-Allow-Origin: *)<img src="..."> in your appGET /api/map/export/kml
KML file of all geocoded restaurants for Google Maps / Google Earth.
Menu data updates monthly (scrapes run on the 1st). Recommended cache durations:
| Data Type | Cache Duration | Notes |
|---|---|---|
| Restaurant list & coordinates | 24 hours | Changes rarely |
| Dish data | 24 hours | Only updates after monthly scrape |
| Analytics (diversity, stability) | 1 hour | Computed on demand |
| Thumbnails | 24 hours | Already cached server-side |
| Filter options | 24 hours | Stable dataset |
| PDF signed URLs | Do NOT cache the URL | Expires in 1 hour — cache the downloaded PDF file instead |
| Trends data | 1 hour | Aggregated from snapshots |
async function getCachedData(key: string, fetchFn: () => Promise<any>, ttlMs = 86400000) {
const cached = localStorage.getItem(key);
const cacheTime = localStorage.getItem(`${key}_ts`);
if (cached && cacheTime && Date.now() - Number(cacheTime) < ttlMs) {
return JSON.parse(cached);
}
const data = await fetchFn();
localStorage.setItem(key, JSON.stringify(data));
localStorage.setItem(`${key}_ts`, String(Date.now()));
return data;
}
// Usage
const restaurants = await getCachedData('restaurants', async () => {
const res = await fetch('http://localhost:3000/api/map/restaurants');
return res.json();
});
async function getCachedPdf(restaurantId: string): Promise<Blob> {
// Check IndexedDB or your cache for existing file
const cached = await pdfCache.get(restaurantId);
if (cached && cached.age < 7 * 86400000) return cached.blob;
// Get a fresh signed URL
const res = await fetch(`http://localhost:3000/api/restaurants/${restaurantId}/pdf-url`, {
headers: { 'x-pdf-password': '<password>' }
});
const { url } = await res.json();
// Download the actual PDF and cache it locally
const pdfRes = await fetch(url);
const blob = await pdfRes.blob();
await pdfCache.set(restaurantId, blob);
return blob;
}
All /api/v1/* data endpoints require an API key (X-API-Key header) and accepted Terms of Service. See the Quickstart section above.
| Endpoint | Auth Method |
|---|---|
/api/v1/* |
X-API-Key header + terms acceptance |
/api/v1/terms/current |
Public (no auth) |
/api/v1/terms/accept |
X-API-Key only (no terms check) |
/api/restaurants/:id/pdf-url |
x-pdf-password header |
/api/restaurants/:id/pdf-history |
x-pdf-password header |
/api/admin/* |
Admin auth (internal only) |
Rate limit: 100 requests per minute per API key on all /api/v1/ endpoints.
meta block on responses contains API version, terms, and data classification