Skip to main content

Overview

The Public Menu endpoint returns the complete, customer-facing menu for a tenant by subdomain. This is a public, unauthenticated endpoint used by the SYNTIfood frontend to display menus. Authentication: None required (public endpoint) Endpoint: GET /menu/{subdomain} Route Name: food.menu.public

Get Menu by Subdomain

Retrieves the full menu structure including all active categories and items for a given tenant subdomain.
subdomain
string
required
The tenant’s subdomain (e.g., “pizzeria-luigi”)
curl https://app.synticorex.test/menu/pizzeria-luigi
{
  "success": true,
  "menu": {
    "categories": [
      {
        "id": "cat-A3K9",
        "nombre": "Pizzas",
        "foto": "https://example.com/pizzas.jpg",
        "activo": true,
        "items": [
          {
            "id": "item-B7X2",
            "nombre": "Margherita",
            "precio": 12.50,
            "descripcion": "Salsa de tomate, mozzarella fresca, albahaca",
            "image_path": "menu/items/item-B7X2.webp",
            "badge": "Popular",
            "is_featured": true,
            "activo": true,
            "options": [
              {
                "id": "opt_12345",
                "label": "Extra queso",
                "price_add": 2.00
              },
              {
                "id": "opt_67890",
                "label": "Borde relleno",
                "price_add": 3.50
              }
            ]
          },
          {
            "id": "item-C5N8",
            "nombre": "Pepperoni",
            "precio": 14.00,
            "descripcion": "Salsa de tomate, mozzarella, pepperoni",
            "image_path": null,
            "badge": null,
            "is_featured": false,
            "activo": true,
            "options": null
          }
        ]
      },
      {
        "id": "cat-D8M2",
        "nombre": "Bebidas",
        "foto": null,
        "activo": true,
        "items": [
          {
            "id": "item-E4L7",
            "nombre": "Coca-Cola",
            "precio": 2.50,
            "descripcion": "500ml",
            "image_path": null,
            "badge": null,
            "is_featured": false,
            "activo": true,
            "options": null
          }
        ]
      }
    ]
  }
}

Response Structure

FieldTypeDescription
successbooleanWhether the request succeeded
menuobjectMenu data object
menu.categoriesarrayArray of category objects

Category Object

FieldTypeDescription
idstringUnique category identifier
nombrestringCategory name
fotostring | nullCategory photo URL
activobooleanWhether category is active
itemsarrayArray of item objects in this category

Item Object

FieldTypeDescription
idstringUnique item identifier
nombrestringItem name
precionumberItem base price
descripcionstring | nullItem description
image_pathstring | nullRelative path to item image
badgestring | nullBadge text (e.g., “Popular”, “New”)
is_featuredbooleanWhether item should be featured
activobooleanWhether item is active/visible
optionsarray | nullCustomization options

Option Object

FieldTypeDescription
idstringUnique option identifier
labelstringOption display name
price_addnumberAdditional price for this option

Usage Notes

Image URLs: The image_path field contains a relative path. To construct the full URL:
https://app.synticorex.test/storage/tenants/{tenantId}/{image_path}
Example: https://app.synticorex.test/storage/tenants/42/menu/items/item-B7X2.webp
Filtering: The API returns ALL categories and items (both active and inactive). Frontend implementations should filter by the activo field to show only active items to customers.
Performance: This endpoint reads from the filesystem (JSON file) rather than a database. Response times may vary based on menu size. Consider implementing client-side caching for better performance.

Example Integration

React/Next.js

import { useState, useEffect } from 'react';

export default function Menu({ subdomain }) {
  const [menu, setMenu] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchMenu() {
      try {
        const response = await fetch(`/menu/${subdomain}`);
        const data = await response.json();
        
        if (data.success) {
          // Filter only active categories and items
          const activeMenu = {
            ...data.menu,
            categories: data.menu.categories
              .filter(cat => cat.activo)
              .map(cat => ({
                ...cat,
                items: cat.items.filter(item => item.activo)
              }))
          };
          setMenu(activeMenu);
        }
      } catch (error) {
        console.error('Failed to fetch menu:', error);
      } finally {
        setLoading(false);
      }
    }

    fetchMenu();
  }, [subdomain]);

  if (loading) return <div>Loading menu...</div>;
  if (!menu) return <div>Menu not found</div>;

  return (
    <div>
      {menu.categories.map(category => (
        <div key={category.id}>
          <h2>{category.nombre}</h2>
          {category.items.map(item => (
            <div key={item.id}>
              <h3>{item.nombre}</h3>
              <p>{item.descripcion}</p>
              <p>${item.precio}</p>
              {item.options && (
                <ul>
                  {item.options.map(opt => (
                    <li key={opt.id}>
                      {opt.label} (+${opt.price_add})
                    </li>
                  ))}
                </ul>
              )}
            </div>
          ))}
        </div>
      ))}
    </div>
  );
}

Error Handling

HTTP StatusErrorDescription
404tenant_not_foundNo tenant exists with the provided subdomain
200Empty categoriesTenant exists but has no menu configured yet

Data Storage

Menu data is stored in a JSON file at:
storage/app/tenants/{tenantId}/menu/menu.json
This file is automatically created and managed by the Categories API and Items API.

Build docs developers (and LLMs) love